synchronized实现生产者消费者问题,了解虚假唤醒

生产者消费者问题是一个典型的并发问题,我们要解决的就是实现同步。一般我们都会想到synchronized和lock,今天我们用synchronized来实现这个问题,所以要了解synchronized,以及它和lock的区别。

什么是synchronized?

synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。

synchronized与lock的区别
synchronized lock
java的一个关键字 一个接口,有很多的实现类
无法判断锁的状态 可以判断是否获取了锁
会自动释放锁 只能手动在finally中释放锁,否则会死锁
假设A线程获取锁的时候,B线程等待,如果A阻塞了,那么B只能永远等待 lock会尝试去获取锁,有多种获取锁的方式
锁的类型是可重入锁,非公平锁,不可以可以中断的 可重入锁,默然非公平锁(可以手动设置),可以中断
功能单一,适合锁少量的同步代码 API丰富,灵活度高,适合锁大量的同步代码
实现生产者消费者
package JUC;

public class ProducerAndConsumer {

    public static void main(String[] args) {

        Resource resource = new Resource();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    resource.incresement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    resource.decrease();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }

    //资源类
    static class Resource{

        //生产资源,生产者对其加1,消费者对其减1
        private int num = 0;

        public synchronized void incresement() throws InterruptedException {
            //对增加进行判断
            if(num > 0){
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+"->"+num);
            //通知其他线程对其操作
            this.notifyAll();
        }

        public synchronized void decrease() throws InterruptedException {
            //对减少进行判断
            if(num == 0){
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"->"+num);
            //通知其他线程对其操作
            this.notifyAll();
        }
    }
}

synchronized实现生产者消费者问题,了解虚假唤醒_第1张图片

我们可以发现在A提供生产之后,B消费,很好的解决了同步问题。但是我们如果我们增加了两个线程呢?

虚假唤醒

首先我们先给它增加两个线程,看看生产者和消费者是否还会同步。
我们在A和B下面再增加一个生产者,一个消费者

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    resource.incresement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    resource.decrease();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

我们截取一小段截图看看,发现并没有同步了。出现这种现象的原因就是虚假唤醒!


synchronized实现生产者消费者问题,了解虚假唤醒_第2张图片

我们首先来分析为什么会出现这种情况,官方文档是这样说的

线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:
synchronized (obj) {
while ()
obj.wait(timeout);
... // Perform action appropriate to condition
}

首先我们知道了解决方法,即把if语句改成while语句

    static class Resource{

        //生产资源,生产者对其加1,消费者对其减1
        private int num = 0;

        public synchronized void incresement() throws InterruptedException {
            //对增加进行判断
            while(num > 0){
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+"->"+num);
            //通知其他线程对其操作
            this.notifyAll();
        }

        public synchronized void decrease() throws InterruptedException {
            //对减少进行判断
            while(num == 0){
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"->"+num);
            //通知其他线程对其操作
            this.notifyAll();
        }
    }
synchronized实现生产者消费者问题,了解虚假唤醒_第3张图片

但是说到底我们对虚假唤醒的这个概念还是有点模糊,所以我们举一个例子来说明

(1)首先初始num为0,A线程将一个元素入队,此时num=1;
(2)B线程从队列中获取了一个元素,此时num = 0。
(3)D线程也想从队列中获取一个元素,但此时num = 0,D线程便只能进入阻塞(decrease.wait()),等待 num > 0。
(4)这时,C线程将一个元素入队,并调用incresement.notify()唤醒条件变量。
(5) 处于等待状态的D线程接收到C线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
(6) 然而可能出现这样的情况:当D号线程准备获得队列的锁,去获取队列中的元素时,此时B号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,B线程便获得队列的锁,检查到num > 0,就获取到了C号线程刚刚入队的元素,然后释放队列锁。
(7) 等到D线程获得队列锁,判断发现num == 0,B线程“偷走了”这个元素,所以对于D线程而言,这次唤醒就是“虚假”的,它需要再次等待num > 0。

总结

生产者消费者是面试中容易被问到的问题,借此可以扩展一系列的问题。一定要了解synchronized与lock的区别,以及对synchronized的使用。

本文有参考自
https://www.cnblogs.com/tqyysm/articles/9765667.html
java1.8官方文档

你可能感兴趣的:(synchronized实现生产者消费者问题,了解虚假唤醒)