mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 11:05:36 +00:00
Update 计算机操作系统 - 死锁.md
拆分较长的段落。
This commit is contained in:
@@ -207,7 +207,11 @@ synchronized (first) {
|
||||
|
||||
两个线程各反向转账 10 万次,原来的写法每次运行都会卡住,按 id 顺序加锁后都能完成,两个账户的余额仍为 1000(JDK 21 实测,见[附录 A1](#a1-java-线程死锁))。使用 ReentrantLock 时还可以用 tryLock 设置超时,拿不到锁就释放已持有的锁再重试,破坏的是[不可抢占条件](#3-破坏不可抢占条件)。例如获取第二把锁等待 10 ms 仍失败时释放第一把锁,随机等待 1~4 ms 后重试,同样的两个线程在 30~60 ms 内完成(见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。等待时间要随机,否则两个线程可能总是同时释放、同时重试,谁都拿不到两把锁,这称为活锁。
|
||||
|
||||
需要注意的是,用阻塞式的锁时,死锁的线程处于阻塞状态,并不消耗 CPU;用自旋锁形成的死锁则会一直占用 CPU。上例卡住后,两个线程的状态都是 BLOCKED,之后 3 秒内各自消耗的 CPU 时间都不到 0.1 ms(JDK 21 实测,见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。因此 CPU 占用很高时应先按 [CPU 占用过高](Java%20虚拟机.md#cpu-占用过高)排查死循环、频繁 GC 等问题;CPU 很低、请求却全部卡住,线程都停在加锁的地方,才更像死锁,用 jstack 或 jcmd 的 Thread.print 命令打印线程栈确认。JVM 不会像 InnoDB 那样选一个线程回滚,卡在 synchronized 上的线程也无法被中断,通常只能重启进程,因此要靠固定加锁顺序或 tryLock 提前预防。程序中也可以定时调用 ThreadMXBean 的 findDeadlockedThreads() 检测死锁并告警,但它检测不到包含虚拟线程的死锁环。
|
||||
需要注意的是,用阻塞式的锁时,死锁的线程处于阻塞状态,并不消耗 CPU;用自旋锁形成的死锁则会一直占用 CPU。上例卡住后,两个线程的状态都是 BLOCKED,之后 3 秒内各自消耗的 CPU 时间都不到 0.1 ms(JDK 21 实测,见[附录 A4](#a4-死锁线程的状态与-cpu-时间))。
|
||||
|
||||
因此 CPU 占用很高时应先按 [CPU 占用过高](Java%20虚拟机.md#cpu-占用过高)排查死循环、频繁 GC 等问题;CPU 很低、请求却全部卡住,线程都停在加锁的地方,才更像死锁,用 jstack 或 jcmd 的 Thread.print 命令打印线程栈确认。
|
||||
|
||||
JVM 不会像 InnoDB 那样选一个线程回滚,卡在 synchronized 上的线程也无法被中断,通常只能重启进程,因此要靠固定加锁顺序或 tryLock 提前预防。程序中也可以定时调用 ThreadMXBean 的 findDeadlockedThreads() 检测死锁并告警,但它检测不到包含虚拟线程的死锁环。
|
||||
|
||||
一个线程持有锁后再次申请同一把锁,等的是只有自己才会释放的锁,也会卡死,称为自死锁(self-deadlock)。synchronized 和 ReentrantLock 是可重入锁,同一线程再次加锁只把计数加 1;不可重入的锁会卡住,例如许可数为 1 的 Semaphore,同一线程连续 acquire 两次后一直处于 WAITING,findDeadlockedThreads() 也检不出来(JDK 21 实测,见[附录 A5](#a5-单个线程重复加锁))。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user