前言
如果每个线程能够独自完成自己的任务,就最好不过了。但是现实是很多情况下各个线程之间需要沟通和协作,通过相互配合的方式共同完成一个任务。Java
中的Object
类中提供了如下方法,让我们做到能够在完成某些操作时通知(唤醒)一个或多个进程,或者当判定不满足一些条件时,主动释放掉获得的锁,并将自己加入该锁的等待队里。
public final native void wait(long timeout) throws InterruptedException;
public final native void notifyAll(); // 唤醒所有线程
public final native void notify(); // 唤醒一个线程
为什么需要notify()与wait()
结合具体场景分析这两个方法的作用,首先我们假设有一个厨房,厨房外有一把锁,每次只允许一位食客进来吃饭。厨师在厨房里做好饭后,就离开厨房等多个食客进入就餐。下面给出Kitchen类的具体实现:
- Kitchen类
public class Kitchen {
// 厨房的门锁
private Object lock;
// 食物准备好了吗?
private volatile boolean isOk;
public Kitchen(){
this.lock = new Object();
this.isOk = false;
}
// 现在食物准备好了吗?
public boolean isOk() { return isOk;}
// 厨师将该标志设为true,表示师傅已经准备好
public void setOk(boolean ok) { isOk = ok; }
// 该厨房大门上的锁
public Object getLock() { return lock; }
}
我们设立了一个isOk
的标志, 当其为true
时,表示厨师已经把食物准备好了,食客看到这个标志为true
,就明白现在可以就餐了。
我们再编写表示厨师做饭行为的Cook
类,以及表示食客就餐行为的Eat
类,两个类都实现了Runnable
接口的run()
方法,可交给线程执行。
- Cook类
public class Cook implements Runnable {
private Kitchen kitchen;
public Cook(Kitchen kitchen) {
this.kitchen = kitchen;
}
@Override
public void run() {
synchronized (kitchen.getLock()) {
System.out.println("厨师进入厨房准备开始烹饪食物");
// 开始做饭
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 饭做好了,设置isOk变量为true
kitchen.setOk(true);
System.out.println("食物已经做好了,大家可以吃了");
// 离开同步块,释放厨房的锁
}
}
}
Cook类的逻辑很简单,代表厨师的线程会尝试获得厨房上的锁,当成功获得锁进入厨房后,就开始做饭,中间的sleep
方法模拟这个做饭所消耗的时间。当厨师做好饭以后,就把厨房的isOk
标志设置为true
,表示饭做好了,大家可以吃了,然后释放厨房上的锁,离开厨房。
- Eat类
public class Eat implements Runnable {
private Kitchen kitchen;
private String name;
public Eat(Kitchen kitchen, String name) {
this.kitchen = kitchen;
this.name = name;
}
@Override
public void run() {
// 拿到厨房的锁,进入厨房
synchronized (kitchen.getLock()) {
System.out.println(name + "进入了厨房,准备开始吃饭");
// 判断食物做好了吗?没有的话原地等待
while (!kitchen.isOk()) { }
// 食物做好了, 开始吃饭
System.out.println(name + "吃完了饭, 离开了厨房");
}
}
}
当代表食客的线程拿到了厨房的锁,成功进入厨房后,他首先要判断,饭做好了吗?如果没做好,这个食客就开始不停判断饭做没做好,因为isOk
这个条件变量是用volatile
关键字修饰的,保证了可见性,那么只要有厨师把饭做好,那么当前食客线程肯定能感知到,然后从while
循环跳出。但其实这里很明显会发生思索,拿到了厨房锁的食客线程一直自旋等待食物做好,厨师却无法获得锁进去烹饪食物,两者开始了无休止的相互等待。这里我们先放置不管,马上再来解决这个问题。我们先来模拟一种不会发生死锁的理想的场景。
-
一个理想的场景
厨师先进厨房做饭,做好后多个食客进入就餐
给出这个场景下具体的java实现
public class notifyDemo {
public static void main(String[] args) {
Kitchen kitchen = new Kitchen();
Thread cooker = new Thread(new Cook(kitchen));
Thread tom = new Thread(new Eat(kitchen,"tom"));
Thread ben = new Thread(new Eat(kitchen,"ben"));
cooker.start();
// 主线程休眠,让厨师线程先把饭做好
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tom.start();
ben.start();
}
}
cooker线程先执行,然后主线程休眠等待cooker线程把饭做好之后启动tom和ben两个食客线程进去用餐,运行结果如下一切正常。
厨师进入厨房准备开始烹饪食物
食物已经做好了,大家可以吃了
tom进入了厨房,准备开始吃饭
tom吃完了饭, 离开了厨房
ben进入了厨房,准备开始吃饭
ben吃完了饭, 离开了厨房
Process finished with exit code 0
现在让我们颠倒一下顺序,让食客tom线程先运行。
public class notifyDemo {
public static void main(String[] args) {
Kitchen kitchen = new Kitchen();
Thread cooker = new Thread(new Cook(kitchen));
Thread tom = new Thread(new Eat(kitchen,"tom"));
Thread ben = new Thread(new Eat(kitchen,"ben"));
// 先让食客进入,此时发生死锁
tom.start();
// 主线程休眠,让厨师线程先把饭做好
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cooker.start();
ben.start();
}
}
和我们预期的一致,发生了死锁。
tom进入了厨房,准备开始吃饭
//发生死锁,开始无限等待
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
解决这个死锁问题的关键是要在食客进入厨房后,如果发现厨师没有把饭做好,就主动释放厨房的锁,并主动让出cpu
供别的线程使用,过一段时间再试,而不是原地空转。
while (!kitchen.isOk()) {
释放锁,让出cpu,等待饭做好
}
怎么实现呢?让出cpu
似乎很简单,我们只要让食客线程sleep一段时间就好了,让我们试一下。
while (!kitchen.isOk()) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public class notifyDemo {
public static void main(String[] args) {
Kitchen kitchen = new Kitchen();
Thread cooker = new Thread(new Cook(kitchen));
Thread tom = new Thread(new Eat(kitchen,"tom"));
Thread ben = new Thread(new Eat(kitchen,"ben"));
// 先让食客进入,此时发生死锁
tom.start();
// 主线程休眠,让厨师线程先把饭做好
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cooker.start();
//ben.start();
}
}
tom
拿到锁进入厨房后,发现食物还没做好,于是sleep
让出cpu
,随后cooker
线程启动开始做饭。观察一下运行结果。
tom进入了厨房,准备开始吃饭
// 依然发生死锁,因为tom线程sleep的时候没有释放锁
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
最后和之前一样发生了死锁,原因在于当tom
这个线程sleep
的时候,他没有释放掉厨房的锁,后来启动的cooker线程,依然无法拿到锁进去做饭。
为了解决这个问题,就需要借助notify(
)和wait()
方法了。
- 首先要明确这两个方法都是和锁对象进行绑定的,比如上文的
Object lock = new Object()
。使用的时候是在lock这个锁对象上调用wait()
和notify()
方法。- 当我们在一个锁对象上调用
wait()
的方法时,该线程会被加入和这个锁关联的等待队列中,同时时候放掉锁,让出cpu
。- 当在一个锁对象上调用
notify()
方法时,会随机通知等待队列中的一个线程,“我这边准备好了,你可以准备开始争用我要释放的锁了”。- 执行了
notify()
方法的线程继续执行,直到退出同步代码块,释放占用的锁。被通知的线程抢占到锁后继续从之前调用lock.wait()
方法的下一行语句开始继续执行。
整体的运行流程如下:
按照这个逻辑修改代码
- Eat类里添加等待逻辑
public void run() {
// 拿到厨房的锁,进入厨房
synchronized (kitchen.getLock()) {
System.out.println(name + "进入了厨房,准备开始吃饭");
// 判断食物做好了吗?没有的话原地等待
while (!kitchen.isOk()) {
try {
// 饭还没好,把自己加入这个锁的等待队列中去
kitchen.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 食物做好了, 通知等待的食客准备开始吃饭
System.out.println(name + "吃完了饭, 离开了厨房");
}
}
- Cook类里添加通知逻辑
public void run() {
synchronized (kitchen.getLock()) {
System.out.println("厨师进入厨房准备开始烹饪食物");
// 开始做饭
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 饭做好了,设置isOk变量为true
kitchen.setOk(true);
// 通知一个线程,饭已经做好了。
kitchen.getLock().notify();
System.out.println("食物已经做好了,大家可以吃了");
// 离开同步块,释放厨房的锁
}
}
- 测试结果,利用线程间通知机制,死锁问题被解决了。
tom进入了厨房,准备开始吃饭
厨师进入厨房准备开始烹饪食物
食物已经做好了,大家可以吃了
tom吃完了饭, 离开了厨房
Process finished with exit code 0
不要在循环之外调用wait方法!
现在我们来关注Eat
实现里的一个细节
while (!kitchen.isOk()) {
try {
// 饭还没好,把自己加入这个锁的等待队列中去
kitchen.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
当这个线程从wait()
方法中醒来,说明此时厨师已经把饭做好了,isOk
肯定是等于true
的,可是在这里却在while循环里再重新判断了一次isOk
的值,这是不是多此一举呢?现在我们考虑这样一种情况,我们厨师线程不采用notify()
的方法,而采用notifyAll()
方法,通知多个食客进程准备用餐,同时,这回厨师只做了一份食物,因此当一个食客吃完后就把isOk
标志位设为false
,表示饭吃完了。
补充:
notifyAll()
方法与notify()
的区别是,notify()
随机通知一个
线程,而notifyAll()
通知所有等待的线程。
我们修改Eat类和Cook类的执行逻辑,来模拟上述过程。
- Eat类
public void run() {
// 拿到厨房的锁,进入厨房
synchronized (kitchen.getLock()) {
System.out.println(name + "进入了厨房,准备开始吃饭");
// 判断食物做好了吗?没有的话原地等待
if (!kitchen.isOk()) {
try {
// 饭还没好,把自己加入这个锁的等待队列中去
kitchen.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 模拟吃饭所花的时间
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 把饭吃完,设置标志位为false
kitchen.setOk(false);
System.out.println(name + "吃完了饭, 离开了厨房");
}
}
- Cook类
public void run() {
synchronized (kitchen.getLock()) {
System.out.println("厨师进入厨房准备开始烹饪食物");
// 开始做饭
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 饭做好了,设置isOk变量为true
kitchen.setOk(true);
// 通知一个线程,饭已经做好了, 通知所有线程。
kitchen.getLock().notifyAll();
System.out.println("食物已经做好了,大家可以吃了");
// 离开同步块,释放厨房的锁
}
}
- Test类
public static void main(String[] args) {
Kitchen kitchen = new Kitchen();
Thread cooker = new Thread(new Cook(kitchen));
Thread tom = new Thread(new Eat(kitchen,"tom"));
Thread ben = new Thread(new Eat(kitchen,"ben"));
// 先让食客进入,此时发生死锁
tom.start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cooker.start();
ben.start();
}
- 实验结果
tom进入了厨房,准备开始吃饭
ben进入了厨房,准备开始吃饭
厨师进入厨房准备开始烹饪食物
食物已经做好了,大家可以吃了
ben吃完了饭, 离开了厨房
tom吃完了饭, 离开了厨房
Process finished with exit code 0
可以发现,即使食物只有一份,tom
和ben
却都吃上了饭,这显然是不可能的。问题的关键在于我们没有把wait()
放在循环里。我们之前提到过,位于某个锁等待队列的线程,被通知并抢占到对应锁后,从wait()
语句的下一行开始执行,我们看看如果用if
语句会发生什么。
根据上述的执行流程我们发现,同时被通知到的食客线程Ben把isOk设置为false,而Tom线程却不再去判断isOk是否为true,吃到了不存在的饭。
从wait()
中恢复的线程,其当初的条件变量很可能又被其他线程修改过,需要重新判断,因此需要被放在while
循环里。
条件判断和notify()/wait()
方法要在一个同步代码块内部!
其实就是要求我们把条件判断和执行notify()/wait()
方法,合并成一个原子方法。否则很可能会发生饥饿现象,考虑如下的执行流程。
Tom
线程发现isOk=false
,准备执行wait()
的方法等待厨师把饭做完,但是由于两者并非原子操作,在执行wait()
方法之前,厨师线程把饭做好,并设置条件变量为isOk=true
,可惜Tom
线程将永远无法发现饭做好了,一直到饿死。