一文看懂wait和notify的虚假唤醒(spurious wakeups)
Posted 业余草
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文看懂wait和notify的虚假唤醒(spurious wakeups)相关的知识,希望对你有一定的参考价值。
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
推荐:https://www.xttblog.com/?p=5257
java 多线程 wait 时为什么要用 while 而不是 if?
对于 java 多线程的wait()
方法,我们在 jdk1.6 的说明文档里可以看到这样一段话:
从上面的截图,我们可以看出,在使用 wait 方法时,需要使用 while 循环来判断条件十分满足,而不是 if,那么我们思考以下,如果使用 if 会怎么样?
为方便讲解,我们来看一个被广泛使用的生产消费的例子。demo 代码如下:
/*
生产和消费
*/
package multiThread;
class SynStack
private char[] data = new char[6];
private int cnt = 0; //表示数组有效元素的个数
public synchronized void push(char ch)
if (cnt >= data.length)
try
System.out.println("生产线程"+Thread.currentThread().getName()+"准备休眠");
this.wait();
System.out.println("生产线程"+Thread.currentThread().getName()+"休眠结束了");
catch (Exception e)
e.printStackTrace();
this.notify();
data[cnt] = ch;
++cnt;
System.out.printf("生产线程"+Thread.currentThread().getName()+"正在生产第%d个产品,该产品是: %c\\n", cnt, ch);
public synchronized char pop()
char ch;
if (cnt <= 0)
try
System.out.println("消费线程"+Thread.currentThread().getName()+"准备休眠");
this.wait();
System.out.println("消费线程"+Thread.currentThread().getName()+"休眠结束了");
catch (Exception e)
e.printStackTrace();
this.notify();
ch = data[cnt-1];
System.out.printf("消费线程"+Thread.currentThread().getName()+"正在消费第%d个产品,该产品是: %c\\n", cnt, ch);
--cnt;
return ch;
class Producer implements Runnable
private SynStack ss = null;
public Producer(SynStack ss)
this.ss = ss;
public void run()
char ch;
for (int i=0; i<10; ++i)
// try
// Thread.sleep(100);
// catch (Exception e)
ch = (char)('a'+i);
ss.push(ch);
class Consumer implements Runnable
private SynStack ss = null;
public Consumer(SynStack ss)
this.ss = ss;
public void run()
for (int i=0; i<10; ++i)
/*try
Thread.sleep(100);
catch (Exception e)
*/
//System.out.printf("%c\\n", ss.pop());
ss.pop();
public class TestPC2
public static void main(String[] args)
SynStack ss = new SynStack();
Producer p = new Producer(ss);
Consumer c = new Consumer(ss);
Thread t1 = new Thread(p);
t1.setName("1号");
t1.start();
/*Thread t2 = new Thread(p);
t2.setName("2号");
t2.start();*/
Thread t6 = new Thread(c);
t6.setName("6号");
t6.start();
/*Thread t7 = new Thread(c);
t7.setName("7号");
t7.start();*/
上面的代码只有一个消费者线程和一个生产者线程,程序运行完美,没有任何错误,那为为什么 jdk 里面强调要用 while 呢?
这个问题,在我刚入行的时候,我也看不懂。那时也想了很久,然后遇到了一个好的 CTO 点拨了我一下。这个程序如果用到多个生产者和消费者的情况,就会出错。然后,我将信将疑的试了一下,确实会出错。但是我不能明白为什么就会出错,继续问他,他看我好学的劲头,满意的笑了笑:“看好你的未来!”。
昨天,微信群里有一个网友在面试时,被问到了 wait 方法为什么必须写在 while 循环中?他没回答出来。
而且,这个问题看了 demo 代码后,还提问到,不是有 synchronized 关键字加锁了吗?哪里还有问题?
如果你也有这样的疑问,那说明你对 wait 方法原理的实际运行效果不是很了解,或者也存在错误的理解。我在群里对他们说,在 wait 方法的前后都加上输出提示语句,后来的打印结果出乎他们意料。
一个线程执行了 wait 方法以后,它不会再继续执行了,直到被 notify 唤醒。
那么唤醒以后从何处开始执行?
这是解决这里出错原因的关键。
我们尝试修改代码,实现一个生产线程,两个消费线程。
/*
生产和消费
*/
package multiThread;
class SynStack
private char[] data = new char[6];
private int cnt = 0; //表示数组有效元素的个数
public synchronized void push(char ch)
if (cnt >= data.length)
try
System.out.println("生产线程"+Thread.currentThread().getName()+"准备休眠");
this.wait();
System.out.println("生产线程"+Thread.currentThread().getName()+"休眠结束了");
catch (Exception e)
e.printStackTrace();
this.notify();
data[cnt] = ch;
++cnt;
System.out.printf("生产线程"+Thread.currentThread().getName()+"正在生产第%d个产品,该产品是: %c\\n", cnt, ch);
public synchronized char pop()
char ch;
if (cnt <= 0)
try
System.out.println("消费线程"+Thread.currentThread().getName()+"准备休眠");
this.wait();
System.out.println("消费线程"+Thread.currentThread().getName()+"休眠结束了");
catch (Exception e)
e.printStackTrace();
this.notify();
ch = data[cnt-1];
System.out.printf("消费线程"+Thread.currentThread().getName()+"正在消费第%d个产品,该产品是: %c\\n", cnt, ch);
--cnt;
return ch;
class Producer implements Runnable
private SynStack ss = null;
public Producer(SynStack ss)
this.ss = ss;
public void run()
char ch;
for (int i=0; i<10; ++i)
// try
// Thread.sleep(100);
//
// catch (Exception e)
//
ch = (char)('a'+i);
ss.push(ch);
class Consumer implements Runnable
private SynStack ss = null;
public Consumer(SynStack ss)
this.ss = ss;
public void run()
for (int i=0; i<10; ++i)
/*try
Thread.sleep(100);
catch (Exception e)
*/
//System.out.printf("%c\\n", ss.pop());
ss.pop();
public class TestPC2
public static void main(String[] args)
SynStack ss = new SynStack();
Producer p = new Producer(ss);
Consumer c = new Consumer(ss);
Thread t1 = new Thread(p);
t1.setName("1号");
t1.start();
/*Thread t2 = new Thread(p);
t2.setName("2号");
t2.start();*/
Thread t6 = new Thread(c);
t6.setName("6号");
t6.start();
Thread t7 = new Thread(c);
t7.setName("7号");
t7.start();
上面代码就是在 main 函数里增加了一个消费线程。
然后错误出现了。
数组越界,为什么会这样?
问题的关键就在于7号消费线程唤醒了 6 号消费线程,而 6 号消费线程被唤醒以后,它从哪里开始执行是关键!!!!
它会执行
System.out.println("消费线程"+Thread.currentThread().getName()+"休眠结束了");
这行代码。
不是从 pop() 方法的开始处执行。
那么这跟使用 if 方法有什么关系?
因为,7 号线程唤醒了 6 号线程,并执行了以下 4 行代码。
ch = data[cnt-1];
System.out.printf("消费线程"+Thread.currentThread().getName()+"正在消费第%d个产品,该产品是: %c\\n", cnt, ch);
--cnt;
return ch;
7 号线程执行完上面的代码后,cnt 就 =0 了
又因为 6 号线程被唤醒时已经处在 if 方法体内,它不会再去执行 if 条件判断,所以就顺序往下执行,这个时候执行
ch = data[cnt-1];
// 就会出现越界异常。
// 假如使用 while 就不会,因为当唤醒了 6 号线程以后,它依然会去执行循环条件检测。
// 所以不可能执行下去,保证了程序的安全。
结论:就是用 if 判断的话,唤醒后线程会从 wait 之后的代码开始运行,但是不会重新判断 if 条件,直接继续运行 if 代码块之后的代码,而如果使用 while 的话,也会从 wait 之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行 while 代码块之后的代码块,成立的话继续 wait
。
这种现象,也就是 JDK 文档中提到的虚假唤醒,也有人称为:异常唤醒,虚拟唤醒、伪唤醒。
虚假唤醒(spurious wakeup),是不想唤醒它或者说不确定是否应该唤醒,但是被唤醒了。对程序来说,wait 方法应该卡住当前程序,不应该往后执行;但是实际上并没有被卡住,而是在非预期的时间程序正常执行了,没有程序没有被卡住就是被虚假唤醒了
。
用 while 而不是 if 来判断,可以避免虚假唤醒。是因为操作系统的通知不可信,自己再校验一次,如果是虚假唤醒就再 wait 一次(直到正确为止)。
虚假唤醒是很多语言都存在的问题,也是很多操作系统底层的问题,与具体应用无关。
我列举的生产者消费者例子,我在用通俗的白话解释一下。
-
在单消费者和单生产者的模式中,因为只有两个线程,消费者 pop 方法 notify 通知到的一定是生产者线程,使其执行 push 操作。
-
在多消费者模式中,消费者 pop 方法 notify 随机通知一个 SysStack 对象等待池中的线程,使其进入 SysStack 对象的锁池中竞争获取该对象的锁。产生错误的关键原因在于 notify 通知到的线程既可能是生产者线程有可能是消费者线程。若仅剩一个元素时,某消费者线程执行 pop 方法,判断 if 条件不成立,执行 notify 唤醒了另外的消费者线程,并消费了当前的最后一个元素。被唤醒的消费者线程由于已经在 if 方法中,不需要再判断剩余的元素数量,又紧接着执行了消费一个元素的操作,此时无元素可消费,程序就异常了。
最后,我再补充下多消费者模式代码中如果换成 while,且逻辑不正确时很容易发生程序挂起问题。
因为使用 notify 仍存在导致程序挂起的风险。这里先说一下对象的锁池和等待池。执行 wait 方法会使线程释放锁进入锁对象的等待池。notify 和 notifyAll 通知等待池中的线程,使其进入锁池竞争锁资源。notify 仅仅通知等待池中的一个线程,使其进入锁池竞争锁资源,若竞争到了锁,线程就 running;notifyAll 会通知锁对象的等待池中的所有线程进入锁池竞争锁,尽管最后只能有一个线程得到锁,剩下的都还等着锁资源再释放去竞争。还是举多消费者的例子,若仅剩一个元素时,某消费者线程执行 pop 方法,判断 if 条件不成立,执行 notify 唤醒了另外的消费者线程,并消费了当前的最后一个元素。被唤醒的消费者线程由于已经使用了 while 进行优化,会执行 wait 操作释放锁并加入等待池。此时,若前面全部使用 notify,就会出现锁池中没有线程(都在等待池等着 notify/notifyAll),无人竞争已被释放的锁的情况,这样所有线程都无法 running,程序就被挂起了。
以上知识,如有疑问,欢迎加我微信:codedq,进群沟通。如果觉得内容还可以,欢迎点赞!
以上是关于一文看懂wait和notify的虚假唤醒(spurious wakeups)的主要内容,如果未能解决你的问题,请参考以下文章
一文看懂wait和notify的虚假唤醒(spurious wakeups)
JUC并发编程 --wait 和 sleep的区别 & 加锁对象的小建议 & wait notify 的正确姿势 & 虚假唤醒
Java-JUC:使用wait,notify|notifyAll完成生产者消费者通信,虚假唤醒(Spurious Wakeups)问题出现场景,及问题解决方案。