为什么虚假唤醒可以通过 while避免 ,if却不能呢?

2022-07-27,,

虚假唤醒<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 也会被唤醒后输出结果; 这明显出现了 错误结果

错误结果分析

在进行错误结果分析前需要记住通过以上代码得到的两个结论:

  1. 线程被唤醒后,会从 wait() 处开始继续往下执行;
  2. 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();
                    }

总结

  1. 线程如果进入等待状态,被唤醒后从wait()开始向下继续执行代码;
  2. 如果要用 if 判断线程的运行条件,最好与else相结合;

本文地址:https://blog.csdn.net/hgmolk/article/details/109855802

《为什么虚假唤醒可以通过 while避免 ,if却不能呢?.doc》

下载本文的Word格式文档,以方便收藏与打印。