为什么虚假唤醒可以通过 while避免 ,if却不能呢?
虚假唤醒<spurious wakeups>
-----写在前面: 最近学习java因为这个问题困扰了好几天,结果发现是因为一个特别明显的原因。因此写下这篇文章提醒自己! 与君共勉!
虚假唤醒 Spurious wakeiups 指 :在线程的 等待/唤醒 的过程中,等待的线程被唤醒后,在条件不满足的情况依然继续向下运行了。
Java官方给的Api 的代码块如下
synchronized (obj) {
while (<condition does not hold> and <timeout not exceeded>) {
//.....省略.......
obj.wait(timeoutMillis, nanos);
}
}
那么,为什么官方推荐使用 while 关键字 而不是 if 关键字呢?
while or if ?
我们先看以下代码
public class WhyWhile {
//判断线程是否运行的条件,
static int control;
//锁对象;
static final Object lock = new Object();
//主线程;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//只有control == 1才打印的线程;
new Thread(() -> {
while (true) {
synchronized (lock) {
**while** (control != 1) {
System.out.println("A wait 前");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A wait 后");
}
System.out.println("== 1");
lock.notifyAll();
}
}
},"Thread-1").start();
//只有control == 2才打印的线程;
new Thread(() -> {
**while** (true) {
synchronized (lock) {
while (control != 2) {
System.out.println("B wait 前");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B wait 后");
}
System.out.println("== 2");
lock.notifyAll();
}
}
},"Thread-2").start();
while (true) {
control = sc.nextInt();
//输入后,通知所有需要lock的线程
synchronized (lock) {
lock.notifyAll();
}
}
}
}
以上代码包含了 三个线程 :main,Thread-1(下文用 线程1 代替),Thread-2(下文用 线程2 代替)。以上代码简要如下
control == 1 | 线程1 打印 “ == 1” |
---|---|
control == 2 | 线程1 打印 “ == 2” |
control != 1 and control != 2 | 线程 1 和 线程 2 进入到等待状态 |
– | main 线程是用来 设置 数字control,之后通知所有线程 |
运行以上代码;
输入1后,我们可以看到如下结果;
这说明:当前 线程1 和 线程2 都进入到了等待状态;
尝试输入一个1
可以看到 输入1以后,线程1 被唤醒,从lock.wait()下一行开始运行,也就是从等待后的位置开始运行;
继续往下看结果,可以看到如下
可以看到,线程2在次过程中也被 lock.notifyAll()唤醒过 (这里指 线程1 的代码块中的notifyAll)
如过我们把 线程2 中的 while 换成 if 呢?
//只有control == 2才打印的线程;
new Thread(() -> {
while (true) {
synchronized (lock) {
//只把 while 修改成了 if
if (control != 2) {
System.out.println("B wait 前");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B wait 后");
}
System.out.println("== 2");
lock.notifyAll();
}
}
}).start();
接下来我们再运行一次,同样输入 1;
可以看见,有时候 线程2 也会被唤醒后输出结果; 这明显出现了 错误结果;
错误结果分析
在进行错误结果分析前需要记住通过以上代码得到的两个结论:
- 线程被唤醒后,会从 wait() 处开始继续往下执行;
- while 被掉换成 if 后出现了虚假唤醒,出现了我们不想要的结果;
While 和 if 特点
if(condition){
代码块.....
}
输出语句....
如上,if 判断condition为 true后,会
先执行代码块,再执行输出语句
先执行代码块,再执行输出语句
先执行代码块,再执行输出语句。
通过这一条,我们分析 线程2 的while被替换成if后,以及替换前的代码的运行顺序;
首先是 if
if (control != 2) {
System.out.println("B wait 前");
try {
//1.在这等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.被唤醒后输出"B wait 后";
System.out.println("B wait 后");
}
//3.跳出if输出"== 2";
System.out.println("== 2");
//4.通知其他需要锁的对象;
lock.notifyAll();
然后 while 呢?
//3.回到while条件继续判断
while (control != 2) {
System.out.println("B wait 前");
try {
//1.在这等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.被唤醒后输出"B wait 后";
System.out.println("B wait 后");
}
//运行不到,
System.out.println("== 2");
lock.notifyAll();
if 和 while 不同的判断逻辑让使用 while 可以避免虚假唤醒,因为唤醒后继续向下运行,还是需要再次判断条件。而 if 就 直接运行下去了,如果要使用 if 避免虚假唤醒,需要与else搭配使用(如下),或者直接把正确的输出语句放入if代码块中;
if (control != 2) {
System.out.println("B wait 前");
try {
//1.在这等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.被唤醒后输出"B wait 后";
System.out.println("B wait 后");
}else{
System.out.println("== 2");
lock.notifyAll();
}
总结
- 线程如果进入等待状态,被唤醒后从wait()开始向下继续执行代码;
- 如果要用 if 判断线程的运行条件,最好与else相结合;
本文地址:https://blog.csdn.net/hgmolk/article/details/109855802