多线程中消费者-生产者问题与假唤醒问题
参考文献:
狂神说 https://www.cnblogs.com/hellokuangshen/
java编程思想
jdk1.8文档
synchronized方法出现的生产者-消费者问题
底层wait函数在设计之初为了不减慢条件变量操作的效率并没有去保证每次唤醒都是由notify触发,而是把这个任务交由上层应用去实现,即使用者需要定义一个循环去判断是否条件真能满足程序继续运行的需求,当然这样的实现也可以避免因为设计缺陷导致程序异常唤醒的问题。
概念链接:https://www.zhihu.com/question/50892224/answer/280667072
根据大佬的思路,整理了一下自己对产生假唤醒的理解如下:
虚假唤醒,就是在不该执行的时候,线程被唤醒并执行了不应该被执行的代码,就如下代码所示,本意是0与1之间互相切换,但事实上并非如此。谨记在循环等待锁的时候,要记得使用while进行判断,千万不要用if。
利用sychronized关键字产生假唤醒的代码
package 虚假唤醒;
/**
* @author william
* @description
* @Date: 2020-10-22 23:32
*/
public class FakeNoticeThread {
public static void main(String[] args) {
WillHaveFakeNotice haveFakeNotice = new WillHaveFakeNotice();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
haveFakeNotice.increament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
haveFakeNotice.increament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
haveFakeNotice.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
haveFakeNotice.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class WillHaveFakeNotice {
private int count;
public WillHaveFakeNotice() {
this.count = 0;
}
public synchronized void increament() throws InterruptedException {
//判定
if (count != 0) {
System.out.println(Thread.currentThread().getName()+"进入increment,但count不为零,等待");
Thread.sleep(500);
this.wait();
}
count++;
System.out.println(Thread.currentThread().getName() + " set count = " + count);
//通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (count == 0) {
System.out.println(Thread.currentThread().getName()+"进入了decrement,但count=0,等待");
Thread.sleep(500);
this.wait();
}
count--;
System.out.println(Thread.currentThread().getName() + " set count = " + count);
//通知
this.notifyAll();
}
}
某一次运行的结果:
首先,wait方法将会释放锁并进入等待队列中等待被唤醒继续执行,此时其它在等待队列中的线程将会通过notify(随即选一个线程唤醒)或notifyAll(大家一起唤醒)被唤醒开始抢锁。
D之所以在中间能够一直唤醒,其根本原因除了脸好能一直抢到锁外,还在于,第四行代码中,D抢到了锁,但是判定count=0,需要等待,随即进入wait,但是当其他线程抢到锁,同样也不满足条件需要等待并使用notifyAll告知所有在等待队列中的线程一起抢锁的时候,如果D再次抢到锁,那么就会从刚才this.wait()挂起的地方,往下直接执行count–,当D此次完成后再次抢到锁,那么if条件判断将会失效,从而导致出现负数。
这,大抵就是我所理解的生产者-消费者问题中的虚假唤醒。
A set count = 1
A进入increment,但count不为零,等待
D set count = 0
D进入了decrement,但count=0,等待
C进入了decrement,但count=0,等待
B set count = 1
B进入increment,但count不为零,等待
C set count = 0
C进入了decrement,但count=0,等待
D set count = -1
D set count = -2
D set count = -3
D set count = -4
D set count = -5
D set count = -6
D set count = -7
D set count = -8
D set count = -9
A set count = -8
A进入increment,但count不为零,等待
C set count = -9
C set count = -10
C set count = -11
C set count = -12
C set count = -13
C set count = -14
C set count = -15
C set count = -16
C set count = -17
B set count = -16
B进入increment,但count不为零,等待
A set count = -15
A进入increment,但count不为零,等待
B set count = -14
B进入increment,但count不为零,等待
A set count = -13
A进入increment,但count不为零,等待
B set count = -12
B进入increment,但count不为零,等待
A set count = -11
A进入increment,但count不为零,等待
B set count = -10
B进入increment,但count不为零,等待
A set count = -9
A进入increment,但count不为零,等待
B set count = -8
B进入increment,但count不为零,等待
A set count = -7
A进入increment,但count不为零,等待
B set count = -6
B进入increment,但count不为零,等待
A set count = -5
A进入increment,但count不为零,等待
B set count = -4
B进入increment,但count不为零,等待
A set count = -3
A进入increment,但count不为零,等待
B set count = -2
B进入increment,但count不为零,等待
A set count = -1
B set count = 0
解决办法:将代码中的 if 替换为 while 即可,当线程被唤醒时,将重新判断while条件,从而防止假唤醒问题
同理,对于使用reentrantlock的JUC并发同样也存在这样的问题,解决方案同样是将 if 改成 while 即可