notify产生死锁分析及与notifyAll的区别

线程之间通信有多种方式比如:Lock锁的Condition、CountDownLantch、CyclicBarrier、Semaphore和本文讲到的Object类中的wait()、notify()、notifyAll()方法。wait()、notify()、notifyAll()方法都会释放锁。

线程阻塞和线程等待

  • 线程阻塞:线程调用了sleep()、wait()方法线程进入阻塞状态。阻塞状态的线程放到等待池中。调用notify()、notifyAll()会把等待池中的线程放到锁池中。
  • 线程等待:线程没有竞争到锁(synchronized锁),等待状态的线程放到锁池中。锁释放,锁池中的线程开始抢锁。
    等待池中的线程只有放到锁池中,才可以抢锁。比如notify唤醒线程,唤醒的线程会放到锁池中,与锁池中的线程一同抢锁

等待池、锁池概念及notify和notifyAll的区别。

Reference: 等待池与锁池

notify死锁理论分析

死锁跟竞争资源产生的死锁的概念有点不同,现象相同,线程不再执行。

  • notify产生的死锁:线程都在等待池中,等待被唤醒。若有新的消费者、生产者线程,有一定几率可以唤醒等待池中的线程。理想状态下,等待池不再有线程。即解除死锁
  • 竞争资源产生的死锁:线程互相获取不到锁,相互等待锁。此时线程在锁池中。
    notify产生的死锁有一定几率可救,竞争资源产生的死锁不可救
    为什么notify产生的死锁有一定几率可救?带着这个问题的思考往下看

假设条件:

  • 场景:生产者消费者
  • 队列长度:2
  • 消费者线程个数:4
  • 生产者线程个数:4
  • 唤醒线程使用的方法:notify()。

线程获取锁的流程图
在这里插入图片描述
逻辑分析死锁:
说明:生产者线程以P开头四个线程名简化为P1、P2、P3、P4,消费者线程以C开头四个线程名简化为C1、C2、C3、C4。

1.四个消费者线程先去消费,此时队列为空。四个消费者线程阻塞,进入等待池中,等待被唤醒。如图。
在这里插入图片描述

2.四个生产者线程去生产,P1线程抢到锁把元素放到队列中(其它生产者线程进入到锁池),接着调用notify方法唤醒等待池的线程。一定会唤醒等待池中的线程(此时等待池中只有消费者线程),即会唤醒一个消费者线程,假设唤醒的消费者线程为C4,C4进入到锁池中,与锁池中的其它线程抢锁。假设P4线程抢到锁,此时也会唤醒一个消费者假设C1(此时等待池中只有消费者线程)。假设P2线程抢到锁,但是队列满了,P2线程进入到等待池中,同样P3也进入到等待池中。此时等待池中,有两个生产者P2、P3,两个消费者C2、C3。锁池中C1、C4。
在这里插入图片描述
在这里插入图片描述
3.锁池中的消费者线程C1,C4抢锁。假设C4抢到锁,从队列取出元素。接着调用notify方法唤醒等待池中的线程,假设唤醒的是C2线程。C1抢到锁继续消费。消费之后唤醒的是C3。此时锁池中的两个消费者抢锁,假设抢锁顺序为C2,C3,因队列为空,会再次进入到等待池中。此时等待池中有两个生产者线程P2、P3,两个消费者线程C2、C3,都在等待被唤醒,但是没有线程来唤醒它们,形同死锁。
在这里插入图片描述

在这里插入图片描述
说明以上只是线程执行顺序的一种假设,即可能出现这种情况。

生产者消费者使用notify产生死锁的代码样例

生产者代码

public class Producer extends Thread {
    private Queue<Integer> queue;
    private int maxSize;

    Producer(Queue<Integer> queue, int maxSize) {
        this.queue = queue;
        this.maxSize = maxSize;
    }

    @Override
    public void run() {
        synchronized (queue) {
            while (queue.size() == maxSize) {
                try {
                    System.out.println("已满");
                   queue.wait();
                    System.out.println("已唤醒生产者---");
                } catch (InterruptedException e) {
                    System.out.println("生产者报错");
                }
            }
            Random random = new Random();
            int i = random.nextInt();
            System.out.println("Produce " + i);
            queue.add(i);
            queue.notify();
        }
    }
}

消费者代码

public class Consumer extends Thread {
    private Queue<Integer> queue;
    private int maxSize;

    Consumer(Queue<Integer> queue, int maxSize) {
        this.queue = queue;
        this.maxSize = maxSize;
    }

    @Override
    public void run() {
        synchronized (queue) {
            while (queue.isEmpty()) {
                try {
                    System.out.println("已空");
                    queue.wait();
                    System.out.println("已唤醒消费者---");
                } catch (InterruptedException e) {
                    System.out.println("消费者报错");
                }
            }
            int v = queue.remove();
            System.out.println("Consume " + v);
            queue.notify();
        }
    }
}

main函数测试
为了更大概率的产生死锁现象,把队列大小依旧设置为2。生产者、消费者线程为40个,且先产生40个消费者。

public class ProducerAndConsumer {
    public static void main(String[] args) throws Exception{
        Queue<Integer> queue = new LinkedList<>();
        int maxSize = 2;
        for (int i=0;i<10;i++) {
            Consumer c1 = new Consumer(queue, maxSize);
            Consumer c2 = new Consumer(queue, maxSize);
            Consumer c3 = new Consumer(queue, maxSize);
            Consumer c4 = new Consumer(queue, maxSize);
            c1.start();
            c2.start();
            c3.start();
            c4.start();
        }

        //休眠200ms是为了让尽可能多的消费者进入到等待池中
        Thread.currentThread().sleep(200);

        for (int i=0;i<10;i++) {
            Producer p1 = new Producer(queue, maxSize);
            Producer p2 = new Producer(queue, maxSize);
            Producer p3 = new Producer(queue, maxSize);
            Producer p4 = new Producer(queue, maxSize);
            p1.start();
            p2.start();
            p3.start();
            p4.start();
        }
    }
}

执行两次产生了死锁现象。若你幸运的话执行一次应该就会出现。此时可以用jstack去看下线程快照。windows下执行tasklist | findstr “java” 查找出运行main函数的java进程Id。执行 jstack -l PID >> D:jstack.txt
D:代表是D盘的绝对路径,不加默认下载到当前路径下。

Reference: windows下下载线程快照信息

问题:线程最终是在锁池还是在等待池?还是都有?如何验证?

思路:观察生产者,若打印的Produce、生产者报错加起来为40则证名锁池中无生产者,小于40则证明剩余的线程在锁池和等待池中。观察消费者也同理。
方法①想到的是用wait(3000)等待超时就报错,但此方法不报错,等待三秒之后自动唤醒继续执行。废弃。
方法② 给生产者加一个事件属性。更改后的生产者代码如下:

class Producer extends Thread {
    private Queue<Integer> queue;
    private int maxSize;
    //来记录进来时的时间
    private long currentTime;

    Producer(Queue<Integer> queue, int maxSize) {
        this.queue = queue;
        this.maxSize = maxSize;
    }

    @Override
    public void run() {
        synchronized (queue) {
            while (queue.size() == maxSize) {
                try {
                    System.out.println("已满");
                    //产生的Produce加上报错的,生产者应该等于40。原因若是notify唤醒的则不会报错,因为时间小于3秒。大于三秒的就是阻塞的并且自己唤醒自己的
                    currentTime = System.currentTimeMillis();
                    queue.wait(3000);
                    //大于三秒手动抛异常
                    if(System.currentTimeMillis()-currentTime>=3000)
                        //只要抛出异常被捕获就会接着执行
                        throw  new RuntimeException("报错,自己唤醒的");//抛出的不是捕获的则会不起作用
                    System.out.println("已唤醒生产者---");
                } catch (InterruptedException e) {
                    System.out.println("生产者报错");
                }
            }
            Random random = new Random();
            int i = random.nextInt();
            System.out.println("Produce " + i);
            queue.add(i);
            queue.notify();
        }
    }
}

休眠三秒的原因是确保不是notify唤醒的,notify唤醒不会这么长时间。本人的cup是i7内存16G。若电脑差点就把wait的参数调大,好点就调小。
无论报错还是输出了Produce都代表此线程已执行完毕。

小技巧: ieda输出只能查找内容,不能计算个数。把输出的内容copy出来,copy到sublime Text或者nodepad++或者其它编辑器。查找内容的同时就能看到总个数。刚开始我是一点点数的,哎。。。(掌握技巧的重要性)

线程最终是在锁池还是在等待池

根据以上分析,相信你已知道答案了。最终的线程在等待池中。
总结:

  • 线程调用wait()方法,线程从运行状态进入阻塞状态,线程放到等待池中。唤醒进入锁池,线程状态从阻塞状态到就绪状态(获取到锁就能执行)。

  • 锁池中的线程会执行完毕

    • 正常结束(正常执行完毕,抛出异常)
    • 进入到阻塞状态
  • 解决办法把notify换成notifyAll即可notifyAll会把等待池中的线程都换醒放到锁池中。

注意事项:
synchronized与wait()、notify()、notifyAll()所使用的必须是同一把锁。否则报错。

解答nofify可自救
还是以逻辑分析死锁为例:生产者进线程进来执行的顺序有一种为:
生产者线程–>唤醒生产者P2–>唤醒消费者C2–唤醒生产者P3–>唤醒消费者C3。此时队列还有一个未消费元素。死锁现象解除。

每篇一语

路漫漫其修远兮 ——《离骚》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值