线程之间的虚假唤醒问题常出现在多线程编程中。我看国内很多教程都解释的稀里糊涂的,所以打算写一篇博客好好絮叨絮叨。
首先看一下线程虚假唤醒的定义:
多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们,假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,便是虚假唤醒。
比如:仓库有货了才能出库,突然仓库入库了一个货品;这时所有的线程(货车)都被唤醒,来执行出库操作;实际上只有一个线程(货车)能执行出库操作,其他线程都是虚假唤醒。
接下来我们用一个例子去详细上面这个解释,因为你看这个解释可能已经看蒙了。
我们定义A、C
线程为生产者,负责num+1
//A:num+1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//C:num+1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
我们定义B、D
线程为消费者,负责num-1
//B:num-1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
我们定义num为临界区共享资源,由生产者和消费者读写
private int number=0;
我们完整写下来就是:
package org.example;
/**
* @author linghu
* @date 2023/12/16 16:45
* A num+1
* B num-1
* 顺序:判断->业务->通知
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
//A:num+1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//B:num-1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data{
private int number=0;
//+1
public synchronized void increment() throws InterruptedException {
if (number!=0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notify();
}
//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notify();
}
}
在上面代码中, increment
和 decrement
分别做加法和减法操作。这两个操作由四个线程ABCD去执行,A、C线程执行加法,B、D线程执行减法。
我们执行上面的代码会发生如下:
上图发现,C、D线程执行下来已经出现了-1、、、。这就是我们说的线程虚假唤醒问题。
线程虚假唤醒问题即:
A先执行,执行时调用了
wait
方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。
这里的重点是: wait()
以后会线程会释放锁!由于我们上面用的 if
条件判断 number的值,所以A线程被唤醒执行完毕以后,轮到C线程开始执行的时候,C线程就会跳过下面这个判断:
if(number==0){
//等待
this.wait();
}
直接执行如下代码:
//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){
//等待
this.wait();
}
//上面的判断直接跳过
//直接执行如下代码....
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notify();
}
是的,问题就在于这个 if
判断,导致了线程虚假唤醒。
我们在明确一下上面的结论:
AC线程负责去做加法,首先会判断num的值,如果num不为0,那么两个线程就开始等待,释放锁,这个时候CD线程获得锁去做减法,也会判断num的值,num的值如果不为0.然后开始做减法,做完减法就开始呼唤AC线程。AC线程被呼唤以后,A线程执行完毕,这个时候由于C线程中用了if判断,那么C线程执行的时候,就不会执行if判断了,于是导致了上面的线程虚假唤醒问题。
其实就是把上面线程执行的加法减法方法中的条件if
改成while
即可:
package org.example;
/**
* @author linghu
* @date 2023/12/16 16:45
* A num+1
* B num-1
* 顺序:判断->业务->通知
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
//A:num+1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//B:num-1
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data{
private int number=0;
//+1
public synchronized void increment() throws InterruptedException {
while (number!=0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number==0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notifyAll();
}
}
改成 while
循环以后,A执行线程完毕以后释放锁,C线程才会继续执行while里的判断,这样就避免了if条件只判断一次的尴尬情况。