近期学习java多线程遇到伪唤醒这一新鲜的词汇,伪唤醒是什么?为什么会造成伪唤醒?接下来就以经典的生产者消费者模式为例阐述一下。
什么是线程伪唤醒
这里我用一个例子来说明此问题。假设有两个消费者线程C1、C2和一个生产者线程P,线程通信开始前,产品数量为0,当用wait/notify进行线程通信时,C1执行到wait方法,释放锁等待,此时P生产产品唤醒线程C1,但被C2获取锁消费此产品,C2执行完成后释放锁,此时C1获取锁后进行产品的消费。以上过程出现了只有一个产品,但是被两次消费的现象。参照以下代码:
public class Test {
private final Object lock = new Object();
private int product = 0;
//如果没有产品,在lock对象上等待唤醒,如果有产品,消费.
private Runnable consumer = () -> {
System.out.println(Thread.currentThread().getName() + " prepare consume");
synchronized (lock) {
if (product <= 0) {//替换为while解决线程虚假唤醒问题
try {
System.out.println(Thread.currentThread().getName() + " wait");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " wakeup");
}
product--;
System.out.println(Thread.currentThread().getName() + " consumed product:" + product);
if (product < 0) {
System.err.println(Thread.currentThread().getName() + " spurious lock happend, product: " + product);
}
}
};
//生产一个产品然后唤醒一个在lock对象上等待的consumer
private Runnable producer = () -> {
System.out.println(Thread.currentThread().getName() + " prepare produce");
synchronized (lock) {
product += 1;
System.out.println(Thread.currentThread().getName() + "produced product: " + product);
lock.notify();
}
};
public void producerAndConsumer() {
// 启动2个consumer,1个producer
Thread c1 = new Thread(consumer);
Thread c2 = new Thread(consumer);
Thread p = new Thread(producer);
c1.start();
c2.start();
p.start();
}
public static void main(String[] args) {
//运行100次,以便触发异常现象
for (int i = 0; i < 100; i++) {
new Test().producerAndConsumer();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
代码引用地址
https://www.jianshu.com/p/c68c1fcadd57
为什么会造成伪唤醒
通过以上过程的阐述,造成伪唤醒的根本原因是notify唤醒线程和被唤醒的线程获取锁不是原子操作。在线程被唤醒过程中,如果锁被其他线程抢占执行,等持锁线程执行完后,被唤醒线程获得锁执行,就有可能造成临界资源为0的情况下被过度消费为负数的现象(在生产者消费者模式中)。
怎样避免伪唤醒的发生
综上来看,当我们在线程唤醒后,再加一次对临界资源的判断,就能有效规避此问题,对此,JDK官方给出解决方案在使用wait方法时遵循以下格式:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
此方法用whlie循环在wait后再进行一次临界资源的判断,如果临界资源不满足要求继续设置线程为等待状态,避免了以上过度消费的问题出现。