参考文章
- https://www.jianshu.com/p/25e243850bd2?appinstall=0
- 本文是在阅读完参考文章后,自己的一些实验数据和心得。
前言
- 之前每每提到并发,总会认为
锁
就是条件
,条件
就是锁
,阅读完参考文章,思考了一段时间,明确了锁
和条件
是两个不同的概念: -
条件
:保证业务逻辑的合理性; -
锁
:保证并发结果的正确性,担当程序的辅助角色。
概念解释
- 每个对象都有两个池:
Entry Set
:锁池
- 线程A和B争一把锁,线程A获得了锁,那么线程B进入锁池中,处于Blocked
状态,等待获取锁。Wait Set
:等待池
- 线程A获得锁后,由于条件不满足,调用 wait() 方法后进入等待池,处于Waiting
状态,等待其他线程调用 notify() 或 notifyAll() 来唤醒。 -
wait()
:当前线程立即释放锁,并进入等待池
。notify()
:① 唤醒等待池
中等待特定锁
的一条线程进入锁池
,线程从Waiting
状态变为Blocked
状态;② 同步代码块(以同步代码块为例)结束后,当前线程释放锁。notifyAll()
:① 唤醒等待池
中等待特定锁
的所有线程进入锁池
,线程从Waiting
状态变为Blocked
状态;② 同步代码块(以同步代码块为例)结束后,当前线程释放锁。 -
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())
-
控制台
-
jconsole
-
从结果和线程信息分析得知:1和2 是消费线程,3和4 是生产线程。
-
程序运行结果:(每次结果都可能不同,需多次实验)
A,消费线程1 和 生产线程3 执行完毕,正常退出;
B,消费线程2 和 生产线程4 都在等待池
中,处于Waiting
状态,而且不会有任何线程调用notify()
方法唤醒它们,程序陷入等待地狱
。 -
流程分析
1,线程1、2、3、4争用锁,消费线程1 获取到锁,执行;2、3、4 进入锁池
中等待获取锁
2,① isEmpty = true,消费线程1 进入 while 后 wait(),释放锁并进入等待池
;
② 消费线程2 获得锁,执行
3,① isEmpty = true,消费线程2 进入 while 后 wait(),释放锁并进入等待池
;
② 生产线程3 获得锁,执行
4,① isEmpty = true,生产线程3 跳过 while 循环,设置 isEmpty = false,然后notify()
唤醒 消费线程1 进入锁池
;同步代码块执行完毕后 生产线程3 释放锁并退出;
②锁池
中 消费线程1 和 生产线程4 争用锁,结果生产线程4 获得锁,执行
5,① isEmpty = false,生产线程4 进入 while 后 wait(),释放锁并进入等待池
;
② 消费线程1 获得锁,执行
6,① isEmpty = false,消费线程1 跳出 while 循环,设置 isEmpty = true,然后notify()
唤醒 消费线程2 进入锁池
;同步代码块执行完毕后 消费线程1 释放锁并退出;
②锁池
中 消费线程2 获得锁,执行
7,① isEmpty = true,消费线程2 未能跳出 while 循环,继续 wait(),释放锁并进入等待池
;锁池
中没有线程,即没有线程争用锁;
② 消费线程2 和 生产线程4 都在等待池
中,处于Waiting
状态,而且不会有任何线程调用notify()
方法唤醒它们,程序陷入等待地狱
。
notifyAll()
- 将代码中的
notify()
换成notifyAll()
,就不会出现等待地狱
。 - 控制台
- 程序运行结果:
所有线程执行完毕,正常退出。 - 流程分析:
1,线程1、2、3、4争用锁,消费线程2 获取到锁,执行;1、3、4 进入锁池
中等待获取锁
2,① isEmpty = true,消费线程2 进入 while 后 wait(),释放锁并进入等待池
;
② 消费线程1 获得锁,执行
3,① isEmpty = true,消费线程1 进入 while 后 wait(),释放锁并进入等待池
;
② 生产线程3 获得锁,执行
4,① isEmpty = true,生产线程3 跳过 while 循环,设置 isEmpty = false,然后notifyAll()
唤醒等待池
中 消费线程1 和 2 进入锁池
;同步代码块执行完毕后 生产线程3 释放锁并退出;
②锁池
中 消费线程1、2 和 生产线程4 争用锁,结果生产线程4 获得锁,执行
5,① isEmpty = false,生产线程4 进入 while 后 wait(),释放锁并进入等待池
;
② 消费线程1 获得锁,执行
6,① isEmpty = false,消费线程1 跳出 while 循环,设置 isEmpty = true,然后notifyAll()
唤醒等待池
中 生产线程4 进入锁池
;同步代码块执行完毕后 消费线程1 释放锁并退出;
②锁池
中 消费线程2 与 生产线程4 争用锁,结果 消费线程2 获得锁,执行
7,① isEmpty = true,消费线程2 未能跳出 while 循环,继续 wait(),释放锁并进入等待池
;
② 生产线程4 获得锁,执行
8,① isEmpty = true,生产线程4 跳出 while 循环,设置 isEmpty = false,然后notifyAll()
唤醒等待池
中 消费线程2 进入锁池
;同步代码块执行完毕后 生产线程4 释放锁并退出;
② 消费线程2 获得锁,执行
9,① isEmpty = false,消费线程2 跳出 while 循环,设置 isEmpty = true,然后notifyAll()
;同步代码块执行完毕后 消费线程2 释放锁并退出;
总结
- 以上就是对
notify()
和notifyAll()
的简要分析:notify()
唤醒一条线程(爱谁谁),容易陷入等待地狱
;notifyAll()
唤醒所有线程,会降低效率。 - 同时从流程分析中也可看出使用 while 循环判断而不是 if 判断的原因(一条消费线程和一条生产线程时也可用 if 判断),目的是防止过度生产或消费。