本文通过 wait(),notify(),notifyAll()模拟生产者 - 消费者例子, 说明为什么使用 notify()会发生死锁.
1. 代码示例
1.1 生产者
- package com.example.hxk.thread.demo;
- import java.util.List;
- import java.util.concurrent.TimeUnit;
- /**
- * @author Smith 2019/3/21
- */
- public class Producer implements Runnable{
- List<Integer> cache;
- public void put() throws InterruptedException {
- synchronized (cache) {
- while (cache.size() == 1) {
- try {
- cache.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- TimeUnit.SECONDS.sleep(1);
- cache.add(1);
- System.out.println(Thread.currentThread().getName() + "生产者生产了一条.");
- cache.notify();
- }
- }
- public Producer(List<Integer> cache) {
- this.cache = cache;
- }
- @Override
- public void run() {
- while (true) {
- try {
- put();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
1.2 消费者
- package com.example.hxk.thread.demo;
- import java.util.List;
- /**
- * @author Smith 2019/3/21
- */
- public class Customer implements Runnable {
- List<Integer> cache;
- public Customer(List<Integer> cache) {
- this.cache = cache;
- }
- private void custom() {
- synchronized (cache) {
- while (cache.size() == 0) {
- try {
- cache.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- cache.remove(0);
- System.out.println(Thread.currentThread().getName() + "消费者消费了一条.");
- cache.notify();
- }
- }
- @Override
- public void run() {
- while (true) {
- custom();
- }
- }
- }
1.3 测试代码
1.3.1 一个生产者一个消费者
- package com.example.hxk.thread.demo;
- import java.util.ArrayList;
- import java.util.List;
- /**
- * @author Smith 2019/3/21
- */
- public class Test {
- public static void main(String[] args) {
- List<Integer> cache = new ArrayList<>();
- new Thread(new Producer(cache), "P1").start();
- new Thread(new Customer(cache), "C1").start();
- }
- }
运行结果:
结论:
使用 notify 且一个生产者一个消费者的情况下, 生产和消费有条不紊的运行, 没有任何问题.
1.3.2 一个生产者两个消费者
- package com.example.hxk.thread.demo;
- import java.util.ArrayList;
- import java.util.List;
- /**
- * @author Smith 2019/3/21
- */
- public class Test {
- public static void main(String[] args) {
- List<Integer> cache = new ArrayList<>();
- new Thread(new Producer(cache), "P1").start();
- new Thread(new Customer(cache), "C1").start();
- new Thread(new Customer(cache), "C2").start();
- }
- }
运行结果:
结论:
使用 notify 且一个生产者两个消费者的情况下, 生产了两次后, 程序死锁了.
1.3.3 将 Producer 和 Customer 中的 notify()换成 notifyAll()
代码就不粘了.
运行结果
程序又恢复了正常.
2.notify()和 notifyAll 的区别
每个同步对象都有自己的锁池和等待池.
2.1 锁池和等待池
锁池: 假设线程 A 已经拥有了某个对象 (注意: 不是类) 的锁, 而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块), 由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权, 但是该对象的锁目前正被线程 A 拥有, 所以这些线程就进入了该对象的锁池中.
等待池: 假设一个线程 A 调用了某个对象的 wait()方法, 线程 A 就会释放该对象的锁 (因为 wait() 方法必须出现在 synchronized 中, 这样自然在执行 wait()方法之前线程 A 就已经拥有了该对象的锁), 同时线程 A 就进入到了该对象的等待池中. 如果另外的一个线程调用了相同对象的 notifyAll()方法, 那么处于该对象的等待池中的线程就会全部进入该对象的锁池中, 准备争夺锁的拥有权. 如果另外的一个线程调用了相同对象的 notify()方法, 那么仅仅有一个处于该对象的等待池中的线程 (随机) 会进入该对象的锁池.
2.2 notify()和 notifyAll()的区别
线程调用了 wait()方法, 便会释放锁, 并进入等待池中, 不会参与锁的竞争
调用 notify()后, 等待池中的某个线程 (只会有一个) 会进入该对象的锁池中参与锁的竞争, 若竞争成功, 获得锁, 竞争失败, 继续留在锁池中等待下一次锁的竞争.
调用 notifyAll()后, 等待池中的所有线程都会进入该对象的锁池中参与锁的竞争.
3. 例子解析
知道了 2 的知识后, 上面的三个例子也就好解释了.
1.3.1: 因为只有一个生产者和消费者, 所以等待池中始终只有一个线程, 要么就是生产者唤醒消费者, 要么消费者唤醒生产者, 所以程序可以成功跑起来;
1.3.2: 举个可能的例子
现在有三个线程, 生产者 P1, 消费者 C1 和 C2. 开始运行的时候, 三个都在锁池中等待竞争, 假设 C1 抢到锁了, C1 执行时由于没有资源可以消费 调用 wait()方法, 释放锁并进入等待池.
C2 抢到了锁, 开始消费, 同理, C2 也进入了等待池. 现在锁池里面只剩下了 P1.
P1 获得了锁, 开始生产, 生产完成后, P1 开始调用 notify()方法唤醒等待池中的 C1 或者 C2, 然后 P1 调用 wait()方法释放锁, 并进入了等待池.
假设唤醒的是 C1,C1 进入锁池并获得锁, 消费后 notify()方法唤醒了 C2,C2 进入锁池, C1 进入等待池, 现在锁池中只有 C1.
C1 获得了锁, 发现没有任何资源可以消费, wait()后释放了锁, 进入了等待池, 现在三个线程全都在等待池, 锁池中没有任何线程. 导致死锁!
1.3.3: notifyAll()后, 不存在只唤醒同类线程的情况, 故也就不会出现 1.3.2 死锁的情况.
引用
来源: https://juejin.im/post/5c930141e51d450ad30e43d3