Concurrency - notify() 和 notifyAll()

参考文章

前言

  • 之前每每提到并发,总会认为就是条件条件就是,阅读完参考文章,思考了一段时间,明确了条件是两个不同的概念:
  • 条件:保证业务逻辑的合理性;
  • :保证并发结果的正确性,担当程序的辅助角色。

概念解释

  1. 每个对象都有两个池:
    Entry Set锁池 - 线程A和B争一把锁,线程A获得了锁,那么线程B进入锁池中,处于 Blocked 状态,等待获取锁。
    Wait Set等待池 - 线程A获得锁后,由于条件不满足,调用 wait() 方法后进入等待池,处于 Waiting 状态,等待其他线程调用 notify() 或 notifyAll() 来唤醒。
  2. wait():当前线程立即释放锁,并进入等待池
    notify():① 唤醒等待池中等待特定锁的一条线程进入锁池,线程从 Waiting 状态变为 Blocked 状态;② 同步代码块(以同步代码块为例)结束后,当前线程释放锁。
    notifyAll():① 唤醒等待池中等待特定锁的所有线程进入锁池,线程从 Waiting 状态变为 Blocked 状态;② 同步代码块(以同步代码块为例)结束后,当前线程释放锁。
  3. Waiting Hell等待地狱 - 我自创的词,从最后的结果来看似乎与锁和循环都无关,叫“死锁”或“循环等待”不太合适。

目的

  • 本文主要分析 notify() 造成 Waiting Hell,而 notifyAll() 不会的原因。

示例源码(notify())

package com.deadlock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
 * @author Intuition
 * @date 2019/7/26 20:43
 * @since 0.0.1
 */
public class WaitingHellCausedByNotify {
    public static void main(String[] args) throws InterruptedException {
        Factory factory = new Factory();
        ExecutorService exec = Executors.newCachedThreadPool();

        // 这里 1,2,3,4并不是给线程编号,只是为了与本次实验结果对应,区分 consume 和 produce
        IntStream.rangeClosed(1, 2)
                .forEach(i -> exec.execute(factory::consume));
        IntStream.rangeClosed(3, 4)
                .forEach(i -> exec.execute(factory::produce));

        exec.shutdown();
        // 等待线程执行完毕后再关闭线程
        exec.awaitTermination(60, TimeUnit.MILLISECONDS);
    }
}

class Factory {
    volatile boolean isEmpty = true;

    public void consume() {
        synchronized (this) {
            while (isEmpty) {
                try {
                    System.out.println(Thread.currentThread().getName() + " wait");
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " consume");
            isEmpty = true;
            notify();
            System.out.println(Thread.currentThread().getName() + " notify");
        }
    }

    public void produce() {
        synchronized (this) {
            while (!isEmpty) {
                try {
                    System.out.println(Thread.currentThread().getName() + " wait");
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " produce");
            isEmpty = false;
            notify();
            System.out.println(Thread.currentThread().getName() + " notify");
        }
    }
}

结果与分析(notify())

  • 控制台
    Concurrency - notify() 和 notifyAll()

  • jconsole Concurrency - notify() 和 notifyAll()

  • 从结果和线程信息分析得知:1和2 是消费线程,3和4 是生产线程。

  • 程序运行结果:(每次结果都可能不同,需多次实验)
    A,消费线程1 和 生产线程3 执行完毕,正常退出;
    B,消费线程2 和 生产线程4 都在等待池中,处于 Waiting 状态,而且不会有任何线程调用 notify() 方法唤醒它们,程序陷入等待地狱

  • 流程分析
    1,线程1、2、3、4争用锁,消费线程1 获取到锁,执行;2、3、4 进入锁池中等待获取锁
    Concurrency - notify() 和 notifyAll()
    2,① isEmpty = true,消费线程1 进入 while 后 wait(),释放锁并进入等待池
    ② 消费线程2 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    3,① isEmpty = true,消费线程2 进入 while 后 wait(),释放锁并进入等待池
    ② 生产线程3 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    4,① isEmpty = true,生产线程3 跳过 while 循环,设置 isEmpty = false,然后 notify() 唤醒 消费线程1 进入锁池;同步代码块执行完毕后 生产线程3 释放锁并退出;
    锁池中 消费线程1 和 生产线程4 争用锁,结果生产线程4 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    5,① isEmpty = false,生产线程4 进入 while 后 wait(),释放锁并进入等待池
    ② 消费线程1 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    6,① isEmpty = false,消费线程1 跳出 while 循环,设置 isEmpty = true,然后 notify() 唤醒 消费线程2 进入锁池;同步代码块执行完毕后 消费线程1 释放锁并退出;
    锁池中 消费线程2 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    7,① isEmpty = true,消费线程2 未能跳出 while 循环,继续 wait(),释放锁并进入等待池锁池中没有线程,即没有线程争用锁;
    ② 消费线程2 和 生产线程4 都在等待池中,处于 Waiting 状态,而且不会有任何线程调用 notify() 方法唤醒它们,程序陷入等待地狱
    Concurrency - notify() 和 notifyAll()

notifyAll()

  • 将代码中的 notify() 换成 notifyAll(),就不会出现等待地狱
  • 控制台
    Concurrency - notify() 和 notifyAll()
  • 程序运行结果:
    所有线程执行完毕,正常退出。
  • 流程分析:
    1,线程1、2、3、4争用锁,消费线程2 获取到锁,执行;1、3、4 进入锁池中等待获取锁
    Concurrency - notify() 和 notifyAll()
    2,① isEmpty = true,消费线程2 进入 while 后 wait(),释放锁并进入等待池
    ② 消费线程1 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    3,① isEmpty = true,消费线程1 进入 while 后 wait(),释放锁并进入等待池
    ② 生产线程3 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    4,① isEmpty = true,生产线程3 跳过 while 循环,设置 isEmpty = false,然后 notifyAll() 唤醒等待池中 消费线程1 和 2 进入锁池;同步代码块执行完毕后 生产线程3 释放锁并退出;
    锁池中 消费线程1、2 和 生产线程4 争用锁,结果生产线程4 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    5,① isEmpty = false,生产线程4 进入 while 后 wait(),释放锁并进入等待池
    ② 消费线程1 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    6,① isEmpty = false,消费线程1 跳出 while 循环,设置 isEmpty = true,然后 notifyAll() 唤醒等待池中 生产线程4 进入锁池;同步代码块执行完毕后 消费线程1 释放锁并退出;
    锁池中 消费线程2 与 生产线程4 争用锁,结果 消费线程2 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    7,① isEmpty = true,消费线程2 未能跳出 while 循环,继续 wait(),释放锁并进入等待池
    ② 生产线程4 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    8,① isEmpty = true,生产线程4 跳出 while 循环,设置 isEmpty = false,然后 notifyAll() 唤醒等待池中 消费线程2 进入锁池;同步代码块执行完毕后 生产线程4 释放锁并退出;
    ② 消费线程2 获得锁,执行
    Concurrency - notify() 和 notifyAll()
    9,① isEmpty = false,消费线程2 跳出 while 循环,设置 isEmpty = true,然后 notifyAll() ;同步代码块执行完毕后 消费线程2 释放锁并退出;
    Concurrency - notify() 和 notifyAll()

总结

  • 以上就是对 notify()notifyAll() 的简要分析:notify() 唤醒一条线程(爱谁谁),容易陷入等待地狱notifyAll() 唤醒所有线程,会降低效率。
  • 同时从流程分析中也可看出使用 while 循环判断而不是 if 判断的原因(一条消费线程和一条生产线程时也可用 if 判断),目的是防止过度生产或消费。
上一篇:问二十:说说notify和notifyAll的区别?


下一篇:深入理解wait/notify/notifyAll的作用