Java多线程虚假唤醒问题
Posted wen-pan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程虚假唤醒问题相关的知识,希望对你有一定的参考价值。
1、API解释
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
问题:为什么 if会出现虚假唤醒?
因为if只会执行一次,执行完会接着向下执行if()外边的
而while不会,直到条件满足才会向下执行while()外边的
2、虚假唤醒
①、成员介绍
有三个角色:一个是外卖员,一个是程序员,一个是产品经理。
有两个状态:hasBillsh 和 hasDemand
static boolean hasBills = false;
:表示现在有外卖订单吗?没有订单外卖员不干活。默认没有
static boolean hasDemand = false;
:表示需求清单给出来了吗?没有需求清单程序员不干活。默认没有
有一把锁:static Object lock = new Object();
②、场景介绍
外卖员、程序员、产品经理这三个人上班前都需要去获取锁lock,只有获取到了锁才能正常工作。
程序员获取到锁上班时,首先检查需求清单有没有给出来,如果没有给出来,那么他就wait等待。
外卖员获取到锁上班时,首先检查一下有没有外卖订单,如果没有,那么他就wait等待。
产品经理获取到锁上班时,他会将 hasDemand
的值改为true,表示他给出了需求清单,并且唤醒waitset中正在等待的线程。被唤醒的线程就可以干活了。
③、代码演示
用代码来展示上述流程!!!
public class SpuriousWakeupTest {
// 锁
final static Object lock = new Object();
// 有外卖单吗
static boolean hasBills = false;
// 有给出需求吗
static boolean hasDemand = false;
public static void main(final String[] args) throws InterruptedException {
// 外卖员
new Thread(() -> {
synchronized (lock) {
// 这里用if会有虚假唤醒问题
if (!hasBills) {
System.out.println("没有外卖单,我先歇会儿.......");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (hasBills) {
System.out.println("接到外卖单咯,我去送单了........");
} else {
System.out.println("外卖单都没有,你把我叫醒干什么???");
}
}
}, "外卖员").start();
// 程序员
new Thread(() -> {
synchronized (lock) {
// 这里用if会有虚假唤醒问题
if (!hasDemand) {
System.out.println("需求清单还没给出来不写代码,我先歇会儿.......");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (hasDemand) {
System.out.println("需求清单给出来了,我要写代码了.......");
} else {
System.out.println("需求清单都没给出来,你把我叫醒干什么???");
}
}
}, "程序员").start();
// 主线程休息一下再让产品经理上班(产品经理上班比较晚)
TimeUnit.SECONDS.sleep(1);
// 产品经理,给出需求清单,这时候唤醒waitSet中正在等待的线程
new Thread(() -> {
synchronized (lock) {
System.out.println("我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........");
hasDemand = true;
// 唤醒waitset中的一个
lock.notify();
// 唤醒waitset中的所有
//lock.notifyAll();
}
}, "产品经理").start();
}
}
④、上述代码运行结果及存在的问题
1、运行结果
没有外卖单,我先歇会儿.......
需求清单还没给出来不写代码,我先歇会儿.......
我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........
外卖单都没有,你把我叫醒干什么???
2、运行结果存在的问题
可以看到当产品经理整理好需求文档后,去唤醒waitset中的等待线程,他本来是想只唤醒程序员的,结果他使用的是lock.notify()
方法,一次只能唤醒waitset中的一个,所以他很可能就将外卖员唤醒了,而不是程序员。
此时外卖员被虚假唤醒,并且由于在外卖员线程中使用的是if (!hasBills)
来条件判断是否有外卖订单,当外卖员发现被虚假唤醒后,会直接结束,而不会再次继续等待了。
3、如何解决
关于lock.notify()
方法一次只能唤醒一个的问题,我们可以使用lock.notifyAll()
来一次唤醒waitset中所有等待的线程。
关于使用if (!hasBills)
判断导致被虚假唤醒后不能继续重新等待的问题,我们可以使用while来代替if来解决这个问题。
⑤、改进后的代码
用while代替if,用notifyAll代替notify。
public class SpuriousWakeupTest {
// 锁
final static Object lock = new Object();
// 有外卖单吗
static boolean hasBills = false;
// 有给出需求吗
static boolean hasDemand = false;
public static void main(final String[] args) throws InterruptedException {
// 外卖员
new Thread(() -> {
synchronized (lock) {
// 使用while防止虚假唤醒
while (!hasBills) {
System.out.println("没有外卖单,我先歇会儿.......");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (hasBills) {
System.out.println("接到外卖单咯,我去送单了........");
} else {
System.out.println("外卖单都没有,你把我叫醒干什么???");
}
}
}
}, "外卖员").start();
// 程序员
new Thread(() -> {
synchronized (lock) {
// 使用while防止虚假唤醒
while (!hasDemand) {
System.out.println("需求清单还没给出来不写代码,我先歇会儿.......");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (hasDemand) {
System.out.println("需求清单给出来了,我要写代码了.......");
} else {
System.out.println("需求清单都没给出来,你把我叫醒干什么???");
}
}
}
}, "程序员").start();
TimeUnit.SECONDS.sleep(1);
// 产品经理,给出需求清单,这时候唤醒waitSet中正在等待的线程
new Thread(() -> {
synchronized (lock) {
System.out.println("我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........");
hasDemand = true;
// 一次性唤醒waitset中所有等待lock的线程(也有缺点)
lock.notifyAll();
}
}, "产品经理").start();
}
}
运行结果
没有外卖单,我先歇会儿.......
需求清单还没给出来不写代码,我先歇会儿.......
我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........
需求清单给出来了,我要写代码了.......
外卖单都没有,你把我叫醒干什么???
没有外卖单,我先歇会儿.......
可以看到使用notifyAll + while优化后,可以正确的叫醒程序员和外卖员,并且外卖员在错误的被叫醒后仍然会再次进入等待,并不会自己结束。但是使用notifyAll也有一些问题,比如:一次性叫醒了waitset中所有等待lock锁的线程,多个线程竞争锁,反而增加了系统开销。可以使用condition或park来唤醒指定的线程进行优化。
以上是关于Java多线程虚假唤醒问题的主要内容,如果未能解决你的问题,请参考以下文章