美文网首页
synchronized实现生产者消费者问题,了解虚假唤醒

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

作者: CryFace | 来源:发表于2020-04-20 12:36 被阅读0次

    生产者消费者问题是一个典型的并发问题,我们要解决的就是实现同步。一般我们都会想到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();
            }
        }
    }
    
    

    我们可以发现在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 (obj) {
    while (<condition does not hold>)
    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();
            }
        }
    

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

    (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实现生产者消费者问题,了解虚假唤醒

          本文链接:https://www.haomeiwen.com/subject/kshrihtx.html