线程之间的虚假唤醒问题常出现在多线程编程中。我看国内很多教程都解释的稀里糊涂的,所以打算写一篇博客好好絮叨絮叨。
首先看一下线程虚假唤醒的定义:
多线程环境下,有多个线程执行了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条件只判断一次的尴尬情况。