zoukankan      html  css  js  c++  java
  • notify notifyAll 死锁

    从一个死锁分析wait,notify,notifyAll

    96 
    泡芙掠夺者 
    2017.08.24 22:00* 字数 1361 阅读 249评论 3

    本文通过wait(),notify(),notifyAll()模拟生产者-消费者例子,说明为什么使用notify()会发生死锁。

    1. 代码示例

    1.1 生产者

    public class Producer implements Runnable {
        List<Integer> cache;
    
        public Producer(List<Integer> cache) {
            this.cache = cache;
        }
    
        @Override
        public void run() {
            while (true) {
                produce();
            }
        }
    
        private void produce() {
            synchronized (cache) {
                try {
                    while (cache.size() == 1) {
                        cache.wait();
                    }
    
                    // 模拟一秒生产一条消息
                    Thread.sleep(1000);
                    cache.add(new Random().nextInt());
    
                    cache.notify();
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    1.2 消费者

    public class Consumer implements Runnable {
        List<Integer> cache;
    
        public Consumer(List<Integer> cache) {
            this.cache = cache;
        }
    
        @Override
        public void run() {
            while (true) {
                consume();
            }
        }
    
        private void consume() {
            synchronized (cache) {
                try {
                    while (cache.isEmpty()) {
                        cache.wait();
                    }
    
                    System.out.println("Consumer consumed [" + cache.remove(0) + "]");
                    cache.notify();
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    1.3 测试代码

    1.3.1 一个生产者一个消费者
    public class WaitNotifyTest {
        public static void main(String[] args) throws Exception {
            List<Integer> cache = Lists.newArrayList();
            new Thread(new Consumer(cache)).start();
            new Thread(new Producer(cache)).start();
        }
    }
    
    运行结果:
    Consumer consumed [169154454]
    Consumer consumed [511734]
    Consumer consumed [-25784306]
    Consumer consumed [1648046130]
    ……
    

    从控制台可以看出,每隔一秒钟消费一条数据,完美的生产者消费者模型!

    1.3.2 多个消费者多个生产者

    既然生产者和消费者都继承了Runnable,就应该多线程运行嘛~~

    public class WaitNotifyTest {
        public static void main(String[] args) throws Exception {
            List<Integer> cache = Lists.newArrayList();
            new Thread(new Consumer(cache)).start();
            new Thread(new Consumer(cache)).start();
            new Thread(new Consumer(cache)).start();
    
            new Thread(new Producer(cache)).start();
            new Thread(new Producer(cache)).start();
            new Thread(new Producer(cache)).start();
        }
    }
    
    运行结果:
    Consumer consumed [-1658797021]
    Consumer consumed [-2050633449]
    

    程序运行一会后,控制台就没输出了!!! 通过jstack命令可以分析出当前demo发生了死锁。

    1.3.3 将Consumer和Producer中的notify()换成notifyAll()试试
    运行结果
    Consumer consumed [-781807640]
    Consumer consumed [42787175]
    Consumer consumed [327937050]
    Consumer consumed [2140968760]
    ……
    

    程序又可以欢乐的跑起来了!

    我到底做错什么了!

    2. notify和notifyAll的区别

    1.3.2和1.3.3的不同之处只是将notify换成了notifyAll,肯定是这里有问题

    在说明notify和notifyAll的区别之前,先阐述两个概念:锁池和等待池
    • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
    • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
    再说notify和notifyAll的区别
    • 线程调用了对象的 wait()方法,便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁(即不会参与线程调度,大家理解就好~~)。
    • notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。
    • notify调用后,只会将等待池中的一个随机线程移到锁池。
    知道上面的原理后,1.3.x的例子就很好理解了

    1.3.1:因为只有一个生产者和消费者,所以等待池中始终只有一个线程,要么就是生产者唤醒消费者,要么消费者唤醒生产者,所以程序可以成功跑起来;
    1.3.2:为了简化分析过程,假设两个消费者线程C1、C2,一个生产者线程P1。
    时序过程如下(只是一种可能性,不绝对,毕竟只是个例子嘛~~):

    • C1,C2观察到缓存cache中无数据,进入等待池;
    • P1获取锁并设置cache数据,通过notify唤醒等待池中某个线程C1,假设C1被唤醒并放入锁池,然后P1释放锁、继续循环重新获取锁并因为检测到cache.size()==1而进入等待池;
    • 此时锁池中的线程为C1,C1会竞争到锁,从而消费数据,然后执行notify方法并释放锁,并假设其notify方法会将C2从等待池移入锁池;
    • C2检测到cache为空,执行await()使自身进入锁池。因为自身的阻塞所以不能唤醒C1或P1,从而导致死锁!

    1.3.3:分两种情况分析:

    • cache为空
    • 先假设所有线程P都一直停留在等待池。但是这种情况是不可能存在的,因为cache肯定是某个C线程消费后才为空,然后该C线程会执行notifyAll方法将所有等待池中的线程都移入锁池,所以不可能所有P线程一直在等待池;
    • 既然P线程不可能一直在等待池,那么这种P线程会竞争锁从而设置cache的值导致cache不为空;
    • cache不空的情况分析过程同上

    3 使用wait、notify的基本套路

    下面是effective java中推荐的标准写法:

    synchronized (obj) {
        while (<condition does not hold>)
            obj.wait(timeout);
        // Perform action appropriate to condition
    }
    

    为什么要用while,改成if行不行,就像下面这样:

    synchronized (obj) {
        if (<condition does not hold>)
            obj.wait(timeout);
        // Perform action appropriate to condition
    }
    

    我们将1.1和1.2代码中的while换成if,启动一个生产者,两个消费者线程,某个消费者线程会出现数组下标越界的异常,代码及原因分析如下:

    public class WaitNotifyTest {
        public static void main(String[] args) throws Exception {
            List<Integer> cache = Lists.newArrayList();
            new Thread(new Consumer(cache)).start();
            new Thread(new Consumer(cache)).start();
            Thread.sleep(1000);
            new Thread(new Producer(cache)).start();
        }
    }
    
    • 消费者C1、C2发现cache为空,相继进入等待池;
    • P1生产数据,放入cache并唤醒C1,同时自己进入等待池;
    • C1消费数据,唤醒C2,C2从cache.wait()处开始执行,因为我们将while(cache.isEmpty())改成了if(cache.isEmpty()),C2不会再次检查cache是否为空,而是直接执行后续代码,这时cache的数据已经被C1消费完了,调用cache.get(0)产生数组下标越界!
  • 相关阅读:
    Devexpress之LayoutControl的使用及其控件布局设计
    C#入门笔记3 表达式及运算符2
    C#入门笔记3 表达式及运算符
    C#入门笔记2 变量
    C#入门笔记1
    Devexpress之GridControl显示序列号
    C++学习之重载运算符1
    解决"找不到该项目”无法删除该文件
    删除鼠标右键时“保存至360云盘”
    CSS基础知识——选择器
  • 原文地址:https://www.cnblogs.com/diegodu/p/8462148.html
Copyright © 2011-2022 走看看