Java并发编程的虚假唤醒以及为什么用if做wait判断数据会变乱和为什么用了Synchronized关键字还会感觉像是多个线程同时进入了该方法
一、什么是虚假唤醒?
多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们,假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,便是虚假唤醒。
比如:仓库有货了才能出库,突然仓库入库了一个货品;这时所有的线程(货车)都被唤醒,来执行出库操作;实际上只有一个线程(货车)能执行出库操作,其他线程都是虚假唤醒。
以下为jdk8的文档中,对虚假唤醒的介绍:
文档中说,为了防止虚假唤醒的发生,最好在执行wait()方法的判断条件中用while
循环来判断,而不是if。
二、怎么产生虚假唤醒?
我们定义四个线程,分别是ProducersA、ProducersB、ConsumersC、ConsumersD。当ProducerA或ProducerB线程生产了一个数据后会通知消费者去消费,ConsumerC或ConsumerD消费掉该条数据后会通知生产者去生产,数据的大小为1。也就是说正常情况下,数据只会有0和1两种值,0表示生产者该生产数据了,1表示消费者该消费数据了。
判断条件为只有 cnt 为0时,生产者才能生产,只有当cnt大于0时,消费者才能消费,否则等待。
public class SynchronizedPC { // Synchronized版本的生产者消费者问题
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{ //相当于是在Runnable接口
for (int i = 0; i < 10; i++) {
try {
data.increment(); // 相当于在run方法中调用该方法
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"ProducersA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"ProducersB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"ConsumersC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"ConsumersD").start();
}
}
我们把处理资源的操作放在Data类中,然后用lambda表达式去实现Runnable接口,在run方法中调用对资源的操作。
/*
* 等待,业务处理,唤醒
* 1、如果资源不满足业务处理的要求,该进程就等待。
* 2、资源满足业务处理要求,就进行业务处理
* 3、处理完资源之后,唤醒其他所有进程,看其他进程是否可以远行,回到第一步
* */
class Data{ //资源类
private static volatile int cnt = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (cnt != 0){ // 只能为0才能加
this.wait(); // 等待
}
cnt++;
System.out.println(Thread.currentThread().getName() + "=>" + cnt); //输出调用此方法的线程名字
//通知其他线程我+1完毕
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (cnt == 0){ // 只能不为0才能减
this.wait(); // 等待
}
cnt--;
System.out.println(Thread.currentThread().getName() + "=>" + cnt);
//通知其他线程我-1完毕
this.notifyAll();
}
}
线程如果没有被wait(),并且执行完操作之后,会调用**notifyAll()**方法,唤醒其他所有线程。
我们先用if做**wait()**的条件判断,部分结果如下:
A=>1
C=>0
C=>-1
C=>-2
C=>-3
C=>-4
C=>-5
C=>-6
D=>-7
D=>-8
D=>-9
D=>-10
D=>-11
D=>-12
D=>-13
B=>-12
A=>-11
B=>-10
A=>-9
B=>-8
A=>-7
B=>-6
我一开始的理解是,方法不是用了Synchronized修饰吗,不是同时只能有一个线程操作该方法吗,并且只有cnt为0时才能进行生产,那么cnt不应该超过0才对啊?为什么呢?
三、解释
一开始别人和我说因为产生了虚假唤醒,用虚假唤醒来解释这一行为,我还是很不理解,就算你唤醒几个生产线程程,但是同时只能有一个进入该方法,而且还有判断条件,线程怎么能同时操作方法进行增加呢?
这个方法确实只能同时有一个线程去访问,但是不一定每个访问该方法的线程都会去执行这个if判断条件,这是一个重点。另外一个重点是,我忽略了休眠方法,即wait()。线程被休眠之后,下次被唤醒的话,是直接执行wait()方法之后的代码,即不会再去判断一次cnt的数量,而是直接对cnt进行改变。因为if只能判断一次,只能再第一次休眠的时候判断,重新被唤醒之后不会再执行**wait()**方法,而是执行后面的方法。
这就是为什么这个问题是由虚假唤醒引起的了。
我们把if改成while看看结果:
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersC=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersC=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersD=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersD=>0
ProducersB=>1
ConsumersD=>0
很显然,这次的结果是符合我们的预期的。因为while会不断的对cnt进行判断,第一次被休眠之后,下一次被唤醒还会去判断cnt是否满足条件,不满足的话就继续休眠。意思就是说,if在判断一次之后,就退出if条件判断语句了。而while是一直在进行判断,直到满足条件(或者不符合条件)之后,才能退出循环,才能执行后面的语句。
博文参考自:https://blog.csdn.net/Saintmm/article/details/121092830
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)