Java多线程虚假唤醒问题

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程虚假唤醒问题相关的知识,希望对你有一定的参考价值。

1、API解释

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

问题:为什么 if会出现虚假唤醒?

因为if只会执行一次,执行完会接着向下执行if()外边的
而while不会,直到条件满足才会向下执行while()外边的

2、虚假唤醒

①、成员介绍

有三个角色:一个是外卖员,一个是程序员,一个是产品经理。

有两个状态:hasBillsh 和 hasDemand

static boolean hasBills = false;:表示现在有外卖订单吗?没有订单外卖员不干活。默认没有

static boolean hasDemand = false;:表示需求清单给出来了吗?没有需求清单程序员不干活。默认没有

有一把锁:static Object lock = new Object();

②、场景介绍

外卖员、程序员、产品经理这三个人上班前都需要去获取锁lock,只有获取到了锁才能正常工作。

程序员获取到锁上班时,首先检查需求清单有没有给出来,如果没有给出来,那么他就wait等待。

外卖员获取到锁上班时,首先检查一下有没有外卖订单,如果没有,那么他就wait等待。

产品经理获取到锁上班时,他会将 hasDemand的值改为true,表示他给出了需求清单,并且唤醒waitset中正在等待的线程。被唤醒的线程就可以干活了。

③、代码演示

用代码来展示上述流程!!!

public class SpuriousWakeupTest {

    // 锁
    final static Object lock = new Object();
    // 有外卖单吗
    static boolean hasBills = false;
    // 有给出需求吗
    static boolean hasDemand = false;

    public static void main(final String[] args) throws InterruptedException {

        // 外卖员
        new Thread(() -> {
            synchronized (lock) {
              	// 这里用if会有虚假唤醒问题
                if (!hasBills) {
                    System.out.println("没有外卖单,我先歇会儿.......");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (hasBills) {
                    System.out.println("接到外卖单咯,我去送单了........");
                } else {
                    System.out.println("外卖单都没有,你把我叫醒干什么???");
                }
            }
        }, "外卖员").start();

        // 程序员
        new Thread(() -> {
            synchronized (lock) {
             		// 这里用if会有虚假唤醒问题
                if (!hasDemand) {
                    System.out.println("需求清单还没给出来不写代码,我先歇会儿.......");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (hasDemand) {
                    System.out.println("需求清单给出来了,我要写代码了.......");
                } else {
                    System.out.println("需求清单都没给出来,你把我叫醒干什么???");
                }
            }
        }, "程序员").start();

      	// 主线程休息一下再让产品经理上班(产品经理上班比较晚)
        TimeUnit.SECONDS.sleep(1);

        // 产品经理,给出需求清单,这时候唤醒waitSet中正在等待的线程
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........");
                hasDemand = true;
              	// 唤醒waitset中的一个
	              lock.notify();
              	// 唤醒waitset中的所有
                //lock.notifyAll();
            }
        }, "产品经理").start();

    }
}
④、上述代码运行结果及存在的问题

1、运行结果

没有外卖单,我先歇会儿.......
需求清单还没给出来不写代码,我先歇会儿.......
我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........
外卖单都没有,你把我叫醒干什么???

2、运行结果存在的问题

可以看到当产品经理整理好需求文档后,去唤醒waitset中的等待线程,他本来是想只唤醒程序员的,结果他使用的是lock.notify()方法,一次只能唤醒waitset中的一个,所以他很可能就将外卖员唤醒了,而不是程序员。

此时外卖员被虚假唤醒,并且由于在外卖员线程中使用的是if (!hasBills)来条件判断是否有外卖订单,当外卖员发现被虚假唤醒后,会直接结束,而不会再次继续等待了。

3、如何解决

关于lock.notify()方法一次只能唤醒一个的问题,我们可以使用lock.notifyAll()来一次唤醒waitset中所有等待的线程。

关于使用if (!hasBills)判断导致被虚假唤醒后不能继续重新等待的问题,我们可以使用while来代替if来解决这个问题。

⑤、改进后的代码

用while代替if,用notifyAll代替notify。

public class SpuriousWakeupTest {

    // 锁
    final static Object lock = new Object();
    // 有外卖单吗
    static boolean hasBills = false;
    // 有给出需求吗
    static boolean hasDemand = false;

    public static void main(final String[] args) throws InterruptedException {

        // 外卖员
        new Thread(() -> {
            synchronized (lock) {
                // 使用while防止虚假唤醒
                while (!hasBills) {
                    System.out.println("没有外卖单,我先歇会儿.......");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (hasBills) {
                        System.out.println("接到外卖单咯,我去送单了........");
                    } else {
                        System.out.println("外卖单都没有,你把我叫醒干什么???");
                    }
                }
            }
        }, "外卖员").start();

        // 程序员
        new Thread(() -> {
            synchronized (lock) {
                // 使用while防止虚假唤醒
                while (!hasDemand) {
                    System.out.println("需求清单还没给出来不写代码,我先歇会儿.......");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (hasDemand) {
                        System.out.println("需求清单给出来了,我要写代码了.......");
                    } else {
                        System.out.println("需求清单都没给出来,你把我叫醒干什么???");
                    }
                }
            }
        }, "程序员").start();

        TimeUnit.SECONDS.sleep(1);

        // 产品经理,给出需求清单,这时候唤醒waitSet中正在等待的线程
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........");
                hasDemand = true;
              	// 一次性唤醒waitset中所有等待lock的线程(也有缺点)
                lock.notifyAll();
            }
        }, "产品经理").start();

    }
}

运行结果

没有外卖单,我先歇会儿.......
需求清单还没给出来不写代码,我先歇会儿.......
我是产品经理,终于把需求清单弄好了,可以去交给程序员开发了.........
需求清单给出来了,我要写代码了.......
外卖单都没有,你把我叫醒干什么???
没有外卖单,我先歇会儿.......

可以看到使用notifyAll + while优化后,可以正确的叫醒程序员和外卖员,并且外卖员在错误的被叫醒后仍然会再次进入等待,并不会自己结束。但是使用notifyAll也有一些问题,比如:一次性叫醒了waitset中所有等待lock锁的线程,多个线程竞争锁,反而增加了系统开销。可以使用condition或park来唤醒指定的线程进行优化。

以上是关于Java多线程虚假唤醒问题的主要内容,如果未能解决你的问题,请参考以下文章

JAVA线程虚假唤醒

java多线程 生产者消费者案例-虚假唤醒

多线程中的虚假唤醒问题

多线程下虚假唤醒问题

多线程编程中条件变量和的spurious wakeup 虚假唤醒

一文看懂wait和notify的虚假唤醒(spurious wakeups)