zoukankan      html  css  js  c++  java
  • 生产者消费者和虚假唤醒

    1 定义

    虚假唤醒,即spurious wakeups。wait需要在while循环内使用,原因就是因为存在虚假唤醒。

    2 Monitor

    还是放上这个神图来复习下线程间通信
    monitor

    • 线程在竞争锁失败的情况下会放到Entry Set中,图中2表示线程可以获取锁
    • 获取到锁的线程可以调用wait方法,让线程阻塞,此时线程被放到了Wait Set中,如图中3所示;Wait Set中的线程在时间到或者被notify后可以竞争锁,如图中4所示
    • Wait Set中的线程在获取到锁后才可以继续执行。
    • notify只会将Wait Set中的一个随机线程移到Entry Set来竞争锁,notifyAll会将全部线程由Wait Set移到Entry Set,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在Entry Set等待锁被释放后再次参与竞争。
    • notify和notifyAll的作用可以直观的认为就是将线程从Wait Set挪到Entry Set

    3 虚假唤醒

    我们以生产者消费者问题来举例几种情况,假设在wait在if中而不是在while中
    
    1)情况1 稍微复杂点

    1. 有一个生产者p,两个消费者c1、c2,一个队列queue
    2. c1先执行,由于queue中为0,所以c1调用wait线程阻塞,线程放到了Wait Set中
    3. p生产了一个消费,放到queue中
    4. 在p调用notify之前,c2开始执行,需要竞争queue的锁,所以c2在Entry Set等待竞争锁
    5. p生产完成,调用notify,c1收到被唤醒后,从Wait Set竞争锁,注意此时c2也在竞争锁。
    6. c2从Entry Set先竞争到锁,然后消费了queue中的消息,此时queue大小为0
    7. c2执行完后,释放锁,此时c1竞争到锁,从queue中消费消息,由于queue目前大小为0,所以从queue为0的队列中访问是非法的。

    2)情况2 稍微简单点

    1. 有一个生产者p,两个消费者c1、c2,一个队列queue
    2. c1、c2先启动,由于queue是空,所以分别调用wait,c1、c2都进入Wait Set
    3. 之后p生产了一个消息到queue中,然后调用notifyall,c1和c2都被唤醒。
    4. c1竞争到锁,消费一个消息,queue大小为0,完成后释放锁
    5. c2竞争到锁,消费一条消息,queue大小是-1,抛出异常。

    3)情况3

    1. 假设有两种情况会引起消费者阻塞
    2. c1是由于条件1,调用了wait;c2是由于条件2调用了wait;
    3. p生产了一条消息,然后满足了条件1,调用notify,却唤醒了c2
    4. 由于是使用的if,c2没有再判断是不是条件2被满足了,所以就直接获取到锁,造成错误

    4)情况4

    1. wait可能被interrupt等不被唤醒就继续执行

    总结:其实可以总结为一句话就是wait被唤醒后,可能还是没有满足继续允许的条件,原因可能是被其他线程争夺了也可能是notifyAll了无关的它,所以需要重新去检查下条件

    4 举例

    我们以情况2来举例

    public class ProducerConsumer {
        public static void main(String[] args) throws Exception{
            new ProducerConsumer().test();
        }
    
        public void test() throws InterruptedException {
            Queue<Integer> queue = new LinkedList<>();
            Producer producer = new Producer(queue);
            Consumer consumer = new Consumer(queue, 0);
            Consumer consumer1 = new Consumer(queue, 1);
    
    
            ExecutorService executor = Executors.newFixedThreadPool(6);
            executor.submit(consumer);
            executor.submit(consumer1);
            Thread.sleep(5000);
            executor.submit(producer);
    
    
        }
    
        class Producer implements Runnable {
            private Queue<Integer> queue;
    
            public Producer(Queue<Integer> queue) {
                this.queue = queue;
            }
            @Override
            public void run(){
               Thread.currentThread().setName("consumerThread_" + index);
                while (true) {
                    try {
                        synchronized (queue) {
                            if (queue.size() <= 0) {
                                try {
                                    System.out.println("consumer wait:" + Thread.currentThread().getName());
                                    queue.wait();
                                    System.out.println("consumer wait2:" + Thread.currentThread().getName());
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            System.out.println("gogogo:" + Thread.currentThread().getName());
                            Integer time = new Random().nextInt(100);
                            try {
                                Thread.sleep(time);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println("consumer remove: " + queue.remove() + ": " + Thread.currentThread().getName());
                            queue.notifyAll();
                        }
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                } 
            }
        }
    }
    

    运行结果

    consumer wait:consumerThread_0
    consumer wait:consumerThread_1
    producer notifyall
    consumer wait2:consumerThread_1
    gogogo:consumerThread_1
    consumer remove: 99: consumerThread_1
    consumer wait:consumerThread_1
    consumer wait2:consumerThread_0
    gogogo:consumerThread_0
    java.util.NoSuchElementException
    	at java.util.LinkedList.removeFirst(LinkedList.java:270)
    	at java.util.LinkedList.remove(LinkedList.java:685)
    	at com.hxy.ProducerConsumer$Consumer.run(ProducerConsumer.java:85)
    	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    	at java.lang.Thread.run(Thread.java:745)
    

    上述代码中,两个消费者先启动,由于queue.size 小于等于0 ,所以两个消费者都调用wait,放到了Wait Set中;5s后生产者往队列里发送了一条消息,然后调用notifyAll,两个消费者都被唤醒,由于没有再判断是否满足条件,所以分别获取锁去消费,造成第二个消费者抛出NoSuchElementException异常。

    将上述代码中消费者的if判断修改为while便会正常,运行结果如下:

    consumer wait:consumerThread_0
    consumer wait:consumerThread_1
    producer notifyall
    consumer wait2:consumerThread_1
    gogogo:consumerThread_1
    consumer remove: 53: consumerThread_1
    consumer wait:consumerThread_1
    consumer wait2:consumerThread_0
    consumer wait:consumerThread_0
    

    consumerThread_0竞争到锁后,会从while判断queue.size大小,由于queue大小为0,所以继续wait。

    另外,可能会想到,在while使用wait,对于情况3这种情况,如果调用notifyall,会唤醒其他不相关的线程,而这些线程需要重新判断,然后再调用wait,这显然是一种资源浪费。针对这种情况,我们可以使用Condition,只唤醒相关的线程。

    5 生产者消费者

    下面附上正确的消费者生产者

    public class ProducerConsumer {
        public static void main(String[] args) throws Exception{
            new ProducerConsumer().test();
        }
    
        public void test() throws InterruptedException {
            Queue<Integer> queue = new LinkedList<>();
            Producer producer = new Producer(queue);
            Consumer consumer = new Consumer(queue, 0);
            Consumer consumer1 = new Consumer(queue, 1);
    
            ExecutorService executor = Executors.newFixedThreadPool(6);
            executor.submit(consumer);
            executor.submit(consumer1);
            executor.submit(producer);
    
    
        }
    
        class Producer implements Runnable {
            private Queue<Integer> queue;
    
            public Producer(Queue<Integer> queue) {
                this.queue = queue;
            }
            @Override
            public void run(){
                while (true) {
                    try {
                        synchronized (queue) {
                            while (queue.size() >= 10) { // 防止虚假唤醒
                                queue.wait();
                            }
                            Integer time = new Random().nextInt(100);
    
                            Thread.sleep(time);
    
                            System.out.println("producer add:" + time);
                            queue.add(time);
                            queue.notifyAll();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        class Consumer implements Runnable {
            private Queue<Integer> queue;
            private int index;
    
            public Consumer(Queue<Integer> queue, int i) {
                this.queue = queue;
                this.index = i;
            }
    
            @Override
            public void run() {
                Thread.currentThread().setName("consumerThread_" + index);
                while (true) {
                    try {
                        synchronized (queue) {
                            while (queue.size() <= 0) { // 防止虚假唤醒
                                queue.wait();
                            }
                            Integer time = new Random().nextInt(100);
    
                            Thread.sleep(time);
    
                            System.out.println("consumer remove: " + queue.remove() + ": " + Thread.currentThread().getName());
                            queue.notifyAll(); // 也有可能唤醒另一个consumer中的queue.wait
                        }
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    使用condition的生产者和消费者

    public class ProducerConsumer {
        private Lock lock = new ReentrantLock();
        private Condition notFull = lock.newCondition(); // 两个condition队列
        private Condition notEmpty = lock.newCondition();
    
        public static void main(String[] args) throws Exception{
            new ProducerConsumer().test();
        }
    
        public void test() throws InterruptedException {
            Queue<Integer> queue = new LinkedList<>();
            Producer producer = new Producer(queue);
            Consumer consumer = new Consumer(queue, 0);
            Consumer consumer1 = new Consumer(queue, 1);
    
            ExecutorService executor = Executors.newFixedThreadPool(6);
            executor.submit(consumer);
            executor.submit(consumer1);
            executor.submit(producer);
    
    
        }
    
        class Producer implements Runnable {
            private Queue<Integer> queue;
    
            public Producer(Queue<Integer> queue) {
                this.queue = queue;
            }
            @Override
            public void run(){
                while (true) {
                    lock.lock();
                    try {
                        while (queue.size() >= 10) { // 防止虚假唤醒
                            notFull.await(); // 注意是await,表示等待直到queue不满;可以理解为等待队列不满
                        }
                        Integer time = new Random().nextInt(100);
    
                        Thread.sleep(time);
    
                        System.out.println("producer add:" + time);
                        queue.add(time);
                        notEmpty.signalAll(); // 注意是signal, 写入一条数据,然后通知不空;可以理解为周知队列不空
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    
        class Consumer implements Runnable {
            private Queue<Integer> queue;
            private int index;
    
            public Consumer(Queue<Integer> queue, int i) {
                this.queue = queue;
                this.index = i;
            }
    
            @Override
            public void run() {
                Thread.currentThread().setName("consumerThread_" + index);
                while (true) {
                    lock.lock();
                    try {
                        while (queue.size() <= 0) { // 防止虚假唤醒
                            notEmpty.await(); // 注意是await,表示等待直到queue不空;可以理解为等待队列不空
                        }
                        Integer time = new Random().nextInt(100);
    
                        Thread.sleep(time);
    
                        System.out.println("consumer remove: " + queue.remove() + ": " + Thread.currentThread().getName());
                        notFull.signalAll(); // 注意是signal, 消费了一条数据,然后通知不满;可以理解周知队列不满
                    }catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    }
    

    6 notify死锁问题

    考虑一种场景:队列大小是1,有一个生产者叫做p,两个消费者叫做c1、c2; c1和c2先启动,由于队列为空,所在都wait在wait set; p随后生产了一个消息调用notify,然后由于队列由于队列满了所以wait在wait set;假设p随机唤醒的是c1,那么c1消费完队列中的一个消息后会notify,notify会随机选择一个wait set中的线程,注意,此时c2和p都在wait set,假设唤醒的是c2,则c2由于检查到队列为空则wait,p则无人再唤醒了,这个时候就产生了死锁。如果是调用的notifyAll的话,则p和c2都会被唤醒,就会避免这种情况的发生。可以参考:https://www.jianshu.com/p/45626f4e0fc1

    其实notify死锁的本质原因是notify是随机唤醒的,并不一定能唤醒我们需要的线程

  • 相关阅读:
    Html 中 XHTML Strict 和 Transitional 的区别
    ExtJs 表单验证的方法总结
    C# 获取枚举值描述信息的方法
    VS2005查找替换插件
    自定义(手动调整).net中图像的调色板(How to adjust (customize) image's palette in .net?)
    保存Graphics对象到Bitmap对象
    javascript原生Dictionary类
    javascript中动态加载js、vbs脚本或者css样式表
    各种图像处理类库的比较及选择(The Comparison of Image Processing Libraries)
    .net中比较两幅图片是否相同
  • 原文地址:https://www.cnblogs.com/set-cookie/p/8728907.html
Copyright © 2011-2022 走看看