一、死锁
1、死锁的常见情形之一:同步的嵌套
说明:同步的嵌套,至少得有两个锁,且第一个锁中有第二个锁,第二个锁中有第一个锁。eg:同步代码块中有同步函数,同步函数中有同步代码块。下面的例子,同步代码块的锁是obj,同步函数的锁是this。t1线程先执行同步代码块,获取锁obj,需要锁this才能执行同步函数;而t2线程先执行同步函数,获取锁this,需要锁obj才能执行同步代码块。两个线程相互竞争锁资源,可能和谐的交替执行到最后,也可能会发生死锁
死锁示例:t1线程执行 if 中的内容,先获取obj锁,再获取this锁,接着执行show()方法中的内容。之后出show(),释放this锁,但还没有出同步代码块,没有释放obj锁。此时,t2线程开始执行,走 else 中的内容,拿到this锁,想要获取obj锁。但obj锁被t1线程持有,造成死锁:t1线程持有obj锁,想要this锁;而t2线程持有this锁,想要obj锁。程序挂在这里运行不了
class Ticket implements Runnable {
//票
private int num = 400;
//同步锁
private Object obj = new Object();
//标志位
boolean flag = true;
@Override
public void run() {
if (flag) {
while (true) {
//同步代码块中有同步函数
//同步代码块,锁是obj
synchronized (obj) {
//同步函数,锁是this
show();
}
}
} else {
while (true) {
this.show();
}
}
}
/**
* 同步函数,锁是this
* 同步函数中有同步代码块
*/
public synchronized void show() {
//同步代码块,锁是obj
synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
}
public class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
2、要避免死锁的发生。不容易找到问题在哪儿,发现问题也不好解决
3、写死锁遵循的原则只有一个:嵌套
class Lock {
/**
* static:静态的,类名直接调用即可
* final:锁固定不变
* 嵌套至少得有两个锁
*/
public static final Object loakA = new Object();
public static final Object loakB = new Object();
}
class DeadLock implements Runnable {
//标志位
private boolean flag;
//通过构造函数给标志位赋值
//否则得在主函数中做切换,还得sleep(time),麻烦(见上例)
DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
//加 while循环 是为了让代码多执行几次,以便出现想要的结果
while (true) {
synchronized (Lock.loakA) {
System.out.println(Thread.currentThread().getName() + "...if...lockA...");
synchronized (Lock.loakB) {
System.out.println(Thread.currentThread().getName() + "...if...lockB...");
}
}
}
} else {
while (true) {
synchronized (Lock.loakB) {
System.out.println(Thread.currentThread().getName() + "...else...lockB...");
synchronized (Lock.loakA) {
System.out.println(Thread.currentThread().getName() + "...else...lockA...");
}
}
}
}
}
}
public class Test {
public static void main(String[] args) {
/**
* 1、用构造函数给标志位赋值,做标志位的切换
* 主函数中就不用 切换+sleep(time) 了
*
* 2、线程任务是一个对象,多个线程操作同一个线程任务
* 此处new两个线程任务对象a、b,两个线程t1、t2执行两个线程任务a、b,没问题
* 因为两个线程任务a、b运行的代码都是run()方法,虽然两个对象都有自己的flag,但它们的flag值是固定的,一个是true,一个是false
* 只有这种情况,可以封装多个任务
*
* 3、线程任务封装的资源是布尔型的变量,这个变量虽然在两个线程任务对象中都有独立的一份,但取的值就只能是true或false
* 如果变量不是布尔型,是int,就导致有两个线程任务
* (此处可以封装多个线程任务,是因为DeadLock中除了flag没有其他变量,且flag的值也是固定的(构造函数传参)
* 只有这种情况才可以封装多个线程任务)
*/
DeadLock dl1 = new DeadLock(true);
DeadLock dl2 = new DeadLock(false);
Thread t1 = new Thread(dl1);
Thread t2 = new Thread(dl2);
t1.start();
t2.start();
}
}
二、线程间通信
1、线程间通信:多个线程在处理同一资源,但线程任务不同(之前卖票和存钱的示例,都是多个线程在执行同一个线程任务,即 只有一个run()方法)
2、需求:有一个资源Rsoource,里面有两个属性name和sex。希望输入和输出轮流且不重复进行,即输入一次,输出一次;再输入一次,再输出一次......
(1)代码示例
/**
* 创建一个类来描述资源
* 资源是共享的,操作资源中属性(共享数据)的语句有多条,就存在线程安全问题
*/
class Resource {
String name;
String sex;
//标志位,标记资源中有无数据。默认资源中没有数据,flag=false
//规则:没有数据,就输入;有数据,就输出
//所以,输入和输出前,都要先判断资源中有无数据
boolean flag = false;
}
/**
* 两个任务对象run()要分别封装在两个类中
* 要封装线程任务,需要实现Runnable接口
*/
class Input implements Runnable {
//此处不能new Resource(),否则输入和输出两个线程用的不是同一个资源
//只能将资源Resource r作为参数传递进来。能接收参数传递的两种形式:一般方法和构造函数
//因为线程任务对象一初始化就有资源,所以,使用构造函数传递参数Resource r
private Resource r;
Input(Resource r) {
this.r = r;
}
@Override
public void run() {
//此处使用 int x 变量模拟多用户的切换:%2
//还可以使用 boolean flag 变量做切换,flag = !flag
int x = 0;
while (true) {
//为了保证输入和输出线程使用同一个锁,可以用Resource r作为锁,也可以用静态锁Resource.class等
//锁不能用this,因为this代表本类对象,而输入和输出两个线程在两个类中
synchronized (r) {
//输入前,要先判断资源中有无数据
//如果有数据,不用再输入。此时,Input要停一下,等待Output先输出
//使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
if (r.flag) {
try {
//Input被wait()后,处于冻结状态,释放执行权的同时释放执行资格,只能等待被notify()唤醒
//使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有数据,输入
if (x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "丽丽";
r.sex = "女";
}
//修改标志位。此时,资源中有数据了
//使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
r.flag = true;
//唤醒输出Output线程(因为Input即将进入等待)
//如果Output没有进入等待状态,可以是空唤醒一次
//使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
r.notify();
//模拟多用户的切换
x = (x + 1) % 2; //等价于:x = x++ % 2;
}
}
}
}
/**
* 两个任务对象run()要分别封装在两个类中
* 要封装线程任务,需要实现Runnable接口
*/
class Output implements Runnable {
//此处不能new Resource(),否则输入和输出两个线程用的不是同一个资源
//只能将资源Resource r作为参数传递进来。能接收参数传递的两种形式:一般方法和构造函数
//因为线程任务对象一初始化就有资源,所以,使用构造函数传递参数Resource r
private Resource r;
Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
//为了保证输入和输出线程使用同一个锁,可以用Resource r作为锁。也可以用静态锁Resource.class等
//锁不能用this,因为this代表本类对象,而输入和输出两个线程在两个类中
synchronized (r) {
//输出前,要先判断资源中有无数据
//如果没有数据,不用再输出。此时,Output要停一下,等待Input先输入
//使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
if (!r.flag) {
try {
//Output被wait()后,处于冻结状态,释放执行权的同时释放执行资格,只能等待被notify()唤醒
//使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有数据,输出
System.out.println(r.name + "......" + r.sex);
//修改标志位。此时,资源中没有数据了
//使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
r.flag = false;
//唤醒输入Input线程(因为Output即将进入等待)
//使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
r.notify();
}
}
}
}
public class Test {
public static void main(String[] args) {
//创建资源
Resource r = new Resource();
//创建线程任务。创建任务要明确资源
Input input = new Input(r);
Output output = new Output(r);
//创建线程。创建线程要明确任务
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
//开启线程
t1.start();
t2.start();
}
}
分析:Input和Output共用资源Resource,所以,操作Resource中属性的多条语句需要加同步。主线程首先开启Input,执行其run()方法。判断资源中没有数据flag=false,向其中输入。之后将flag置为true,表示资源中已经有数据了。同时,唤醒Output(Input马上要进入冻结状态了)。此次循环结束,进入下次循环(while(true){...})。判断flag=true,执行wait()方法,Input被冻结。等到Output获取到CPU的执行权,Output执行。先判断flag=true,表示资源中有数据,输出。然后,将flag置为false,表示资源中没有数据。同时,唤醒Input(Output马上要进入冻结状态了)。此次循环结束,进入下次循环(while(true){...})。判断flag=false,执行wait()方法,Output被冻结。等到Input获取到CPU的执行权,再次重复上述操作......
注:创建线程,要明确任务;创建任务,要明确资源
(2)优化后的代码
class Resource {
//资源的属性通常都是私有的,并对外提供访问方法
private String name;
private String sex;
private boolean flag = false;
//不单独写setName()、getName()方法,因为准备同时赋值
//使用同步函数,用this锁更简单
public synchronized void set(String name, String sex) {
if (flag) {
try {
//此时使用的锁是this,代表资源Resource对象
//wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
flag = true;
//wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
//this可省略
this.notify();
}
//使用同步函数,用this锁更简单
public synchronized void out() {
if (!flag) {
try {
//wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + "......" + sex);
flag = false;
//wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
this.notify();
}
}
class Input implements Runnable {
private Resource r;
Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int x = 0;
while (true) {
if (x == 0) {
r.set("mike", "nan");
} else {
r.set("丽丽", "女");
}
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
private Resource r;
Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Input input = new Input(r);
Output output = new Output(r);
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
t1.start();
t2.start();
}
}
说明:资源里面封装了属性(私有的),同时对外提供了访问资源的方法。如果需要同步,在资源所提供的方法中加上同步即可
注:wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
三、等待唤醒机制
1、等待/唤醒机制涉及的方法
(1)wait():让线程处于冻结状态,被wait()的线程会被存储到线程池中(被wait()的线程没有消亡,但失去了CPU的执行资格,存储在线程池中)
注:线程池按照锁来区分
(2)notify():唤醒线程池中的任意一个线程
(3)notifyAll():唤醒线程池中的所有线程(让线程池中的线程都处于运行状态或者临时阻塞状态,即 让线程池中的线程具备CPU的执行资格)
2、使用wait()、notify()、notifyAll()方法的注意事项
(1)wait()、notify()、notifyAll()这些方法都必须定义在同步synchronized(对象){...}中。因为这些方法是用于操作线程状态的方法(监视线程的状态),一旦线程状态发生改变,必须要明确改变的是哪个锁上的线程。所以,在调用这些方法时,要标识出该方法所属的锁
(2)使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
(3)wait()、notify()、notifyAll()不是线程类Thread中的方法,而是Object中的方法。因为当前线程必须拥有此对象监视器,就是锁。而锁可以是任意的对象,任意的对象调用的方法一定定义在Object类中
四、多生产者多消费者问题
生产者生产烤鸭,消费者消费烤鸭,且烤鸭带编号
1、需求:一个生产者和一个消费者,每次 生产/消费 一只烤鸭。只有没有烤鸭时才生产,只有有烤鸭时才消费,其余时间均等待
class Resource {
private String name;
private int count = 1;
//标记
private boolean flag = false;
//传入:名称,得到:名称+编号
//可以使用this锁,所以,使用同步代码块较简单
public synchronized void set(String name) {
if (flag) {
try {
//使用wait()、notify()、notifyAll()要明确所属的锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
this.flag = true;
//使用wait()、notify()、notifyAll()要明确所属的锁
this.notify();
}
public synchronized void out() {
if (!flag) {
try {
//使用wait()、notify()、notifyAll()要明确所属的锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
this.flag = false;
//使用wait()、notify()、notifyAll()要明确所属的锁
this.notify();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true){
r.set("烤鸭");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true){
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
2、需求:两个生产者和两个消费者,每次 生产/消费 一只烤鸭。只有没有烤鸭时才生产,只有有烤鸭时才消费,其余时间均等待
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
//线程封装任务,任务封装资源
//两个生产者
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
//两个消费者
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
问题一:当多个线程生产/消费时,出现了问题:生产一堆烤鸭未被消费,或多次消费同一编号的烤鸭
分析:
(1)生产者t0得到CPU的执行权,进入set()方法,判断flag=false,生产一只烤鸭(生产者...烤鸭1)。之后将flag置为true,执行notify()空唤醒,此次循环结束
(2)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待
(3)生产者t1得到CPU的执行权,进入set()方法,判断flag=true,执行try代码块。t1被wait(),失去CPU的执行资格,进入线程池等待
(4)消费者t2得到CPU的执行权,进入out()方法,判断flag=true,消费一只烤鸭(消费者...烤鸭1)。之后将flag置为false,执行notify(),唤醒生产者线程t0,此次循环结束
(5)因为run()方法中的while(true),进入下次循环,t2再次执行out()方法。此时,flag=false,执行try代码块。t2被wait(),失去CPU的执行资格,进入线程池等待
(6)消费者t3得到CPU的执行权,进入out()方法,判断flag=false,执行try代码块。t3被wait(),失去CPU的执行资格,进入线程池等待
(7)此时只有被唤醒的生产者t0还活着
(8)生产者t0得到CPU的执行权,从wait()处向下走(不再判断if(flag)),生产一只烤鸭(生产者...烤鸭2)。之后将flag置为true,执行notify(),唤醒生产者线程t1,此次循环结束
(9)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待
(10)此时只有被唤醒的生产者t1还活着
(11)生产者t1得到CPU的执行权,从wait()处向下走(不再判断if(flag)),生产一只烤鸭(生产者...烤鸭3)...... 出现问题:生产的烤鸭2未被消费就生产了烤鸭3
原因:被唤醒的线程没有再次判断 if(flag) 标记,就直接执行下面的代码,继续进行生产/消费
做法:将 if(flag) 改为 while(flag),让醒来的线程重新判断标志位flag
问题二:程序死锁
分析(从上面第(8)步开始):
(8)生产者t0得到CPU的执行权,从wait()处醒来。因为while(flag),会再次判断flag=false,生产一只烤鸭(生产者...烤鸭2)。之后将flag置为true,执行notify(),唤醒生产者线程t1,此次循环结束
(9)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待
(10)此时只有被唤醒的生产者t1还活着
(11)生产者t1得到CPU的执行权,从wait()处醒来。因为while(flag),会再次判断flag=true,执行try代码块。t1被wait(),失去CPU的执行资格,进入线程池等待
(12)此时,已经没有活着的线程了,所有线程都被冻结,在线程池等待
原因:notify()唤醒的是任意一方,没有对方导致程序死锁
做法:用notifyAll(),全唤醒(必须要唤醒对方)
正确的代码:
class Resource {
private String name;
private int count = 1;
//标志位
private boolean flag = false;
//传入:名称,得到:名称+编号
//可以使用this锁,所以,使用同步代码块较简单
public synchronized void set(String name) {
//用 while 而不是 if ,是为了让醒来的线程重新判断标志位
while (flag) {
try {
//使用wait()、notify()、notifyAll()要明确所属的锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
this.flag = true;
//使用wait()、notify()、notifyAll()要明确所属的锁
//全唤醒,避免死锁
this.notifyAll();
}
public synchronized void out() {
//用 while 而不是 if ,是为了让醒来的线程重新判断标志位
while (!flag) {
try {
//使用wait()、notify()、notifyAll()要明确所属的锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
this.flag = false;
//使用wait()、notify()、notifyAll()要明确所属的锁
//全唤醒,避免死锁
this.notify();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.set("烤鸭");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
public class Test {
public static void main(String[] args) {
//线程封装任务,任务封装资源
//资源
Resource r = new Resource();
//线程任务
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
//线程
//两个生产者
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
//两个消费者
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
总结:
(1)while:判断标记,多次判断。解决了线程获取执行权后,是否要运行的问题
(2)notifyAll():唤醒全部。保证本方线程一定会唤醒对方线程,避免死锁
(3)if:判断标记,只判断一次。导致不该运行的线程运行,出现了数据错误的情况
(4)notify():只能唤醒一个线程。如果本方唤醒了本方,没有意义。而且 while判断标记+notify() 会导致死锁
依旧存在的问题:
用 while判断标记+notifyAll() ,程序效率有点低。全唤醒时,本方也醒了,而本方判断标记没有意义。只唤醒对方即可