虚假唤醒及避免方式

虚假唤醒

虚假唤醒就是当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用的。
下面看一个生产者消费者的例子

public static class Data {


        private int num = 0;


        public synchronized void increment() {
            if (num > 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "阻塞等待");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num++;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            this.notifyAll();
        }

        public synchronized void decrement() {
            if (num <= 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "阻塞等待");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num--;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            this.notifyAll();
        }
    }

上面的Data类有两个同步方法,increament方法是对num加1,decrement方法是对num减1.
两个都是同步方法,也就是说,我们期望num的值要么是0,要么是1.
通过下面代码,我们创建了两个生产者,两个消费者。

public class ProductConsume {

    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() -> {
            for (int i=0; i<20; i++) {
                data.increment();
            }
        }, "生产者线程A").start();
        new Thread(() -> {
            for (int i=0; i<20; i++) {
                data.increment();
            }
        }, "生产者线程B").start();
        new Thread(() -> {
            for (int i=0; i<20; i++) {
                data.decrement();
            }
        }, "消费者线程C").start();
        new Thread(() -> {
            for (int i=0; i<20; i++) {
                data.decrement();
            }
        }, "消费者线程D").start();

    }
 }

看下运行的结果
在这里插入图片描述
从结果来看,跟我们的期望不太一样,竟然出现了num为2甚至为3的情况。
根据结果中每行的日志,我们逐步分析:

  1. 线程A先执行,进入同步代码块,此时,num初始值为0,满足加1的条件,所以加num = 1;
  2. 线程A执行了notifiAll方法,唤醒其他线程,但是A又继续抢到了cpu执行机会,继续执行第二次循环,这次由于num的值为1,所以线程A阻塞,并释放锁(wait方法释放锁这个动作很关键);
  3. 线程B获取锁,进入increment方法,判断num=1,所以线程B阻塞,并释放锁
  4. 线程C得到cpu执行机会,判断num=1,可以进行减1,并调notifyAll方法唤醒其他线程;
  5. 线程C继续抢到了cpu执行机会,判断num=0,进行阻塞,并释放锁;
  6. 线程B唤醒,对num加1,并调notifyAll方法唤醒其他线程;
  7. 线程B又抢到了cpu执行机会,判断num=1,所以进行wait;
  8. 线程A获取cpu的执行机会,而此时,线程A执行的进度在wait方法处,所以从wait方法后开始执行,所以对num进行了加1操作,导致num的值为2;
  9. 。。。

从上面分析我们知道了,线程A已经进入同步方法内,而且在第6步被notifyAll方法激活为可执行态,所以在第8步抢到cpu执行机会之后,就直接执行后面的代码,导致num增加1而没有判断num值的状态。
这就是虚假唤醒的例子。
为了避免虚假唤醒,可以将increment和decrement方法中的if判断改成while,这样在线程被执行的时候,就会重新判断num的状态,从而避免虚假唤醒问题出现。代码如下:

public static class Data {


        private int num = 0;


        public synchronized void increment() {
            while (num > 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "阻塞等待");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num++;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            this.notifyAll();
        }

        public synchronized void decrement() {
            while (num <= 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "阻塞等待");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num--;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            this.notifyAll();
        }
    }

运行结果如下:
在这里插入图片描述
结果符合预期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值