Update 计算机操作系统 - 死锁.md

补充各种预防方法的代价、死锁避免的前提、死锁线程的状态,附录 A4。
This commit is contained in:
CyC2018
2026-10-29 04:55:49 +08:00
parent 779de28d47
commit ad3c3fd199
+52 -5
View File
@@ -24,6 +24,7 @@
* [A1 Java 线程死锁](#a1-java-线程死锁)
* [A2 MySQL 死锁](#a2-mysql-死锁)
* [A3 先查后插的间隙锁死锁](#a3-先查后插的间隙锁死锁)
* [A4 死锁线程的状态与 CPU 时间](#a4-死锁线程的状态与-cpu-时间)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -103,7 +104,7 @@
## 死锁预防
在程序运行之前预防发生死锁。
在程序运行之前预防发生死锁:四个必要条件缺一不可,设计时破坏其中任意一个,死锁就不可能发生。各种方法的代价不同,实际开发中最常用的是破坏环路等待。
### 1. 破坏互斥条件
@@ -111,7 +112,7 @@
### 2. 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。代价是资源被提前占用、利用率低,而且很多程序事先并不知道需要哪些资源。
### 3. 破坏不可抢占条件
@@ -121,11 +122,11 @@
### 4. 破坏环路等待
给资源统一编号,进程只能按编号顺序来请求资源。
给资源统一编号,进程只能按编号顺序来请求资源。这样所有进程都是从小编号往大编号等待,不可能形成环。它的代价最小,只要求每个加锁的地方遵守同一个顺序,例子见 [Java 线程死锁](#1-java-线程死锁) 和 [MySQL 死锁](#2-mysql-死锁)。
## 死锁避免
在程序运行时避免发生死锁。
在程序运行时避免发生死锁。死锁避免不限制资源的请求方式,而是在每次分配前判断分配之后系统是否仍处于安全状态,不安全就让请求者等待。它要求事先知道每个进程最多需要多少资源,通用的操作系统和应用程序很难满足,因此主要见于教材和特定的资源分配系统。
### 1. 安全状态
@@ -203,7 +204,9 @@ synchronized (first) {
}
```
两个线程各反向转账 10 万次,原来的写法每次运行都会卡住,按 id 顺序加锁后都能完成,两个账户的余额仍为 1000(JDK 21 实测,见[附录 A1](#a1-java-线程死锁))。使用 ReentrantLock 时还可以用 tryLock 设置超时,拿不到锁就释放已持有的锁再重试,破坏的是占有和等待条件。
两个线程各反向转账 10 万次,原来的写法每次运行都会卡住,按 id 顺序加锁后都能完成,两个账户的余额仍为 1000(JDK 21 实测,见[附录 A1](#a1-java-线程死锁))。使用 ReentrantLock 时还可以用 tryLock 设置超时,拿不到锁就释放已持有的锁再重试,破坏的是占有和等待条件。例如获取第二把锁等待 10 ms 仍失败时释放第一把锁,随机等待 1~4 ms 后重试,同样的两个线程在 30~60 ms 内完成(见[附录 A4](#a4-死锁线程的状态与-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 命令打印线程栈确认。程序中也可以定时调用 ThreadMXBean 的 findDeadlockedThreads() 检测死锁并告警。
### 2. MySQL 死锁
@@ -333,9 +336,53 @@ ERROR 1062 (23000) at line 1: Duplicate entry '1024' for key 'payment.PRIMARY'
执行 SET GLOBAL innodb_print_all_deadlocks = ON 后重跑可重复读的例子,错误日志中同样记录了两个事务持有的间隙锁和等待的插入意向锁(lock_mode X locks gap before rec insert intention waiting)。
### A4 死锁线程的状态与 CPU 时间
JDK 21.0.13,macOS(Apple M1 Pro)。用正文中的 transfer 让线程 T1、T2 反向转账直到卡住,等待 5 秒后,用 ThreadMXBean 记录两个线程此后 3 秒内消耗的 CPU 时间,再调用 findDeadlockedThreads():
```java
ThreadMXBean mx = ManagementFactory.getThreadMXBean();
long c1 = mx.getThreadCpuTime(t1.threadId()), c2 = mx.getThreadCpuTime(t2.threadId());
Thread.sleep(3000);
long d1 = mx.getThreadCpuTime(t1.threadId()) - c1, d2 = mx.getThreadCpuTime(t2.threadId()) - c2;
for (ThreadInfo ti : mx.getThreadInfo(mx.findDeadlockedThreads())) { ... }
```
```text
T1 状态 BLOCKED,T2 状态 BLOCKED
之后 3 秒内 T1、T2 消耗的 CPU 时间:62000 ns、35000 ns
findDeadlockedThreads:T1 等待 DeadlockDiag$Account@7291c18f,持有者 T2
findDeadlockedThreads:T2 等待 DeadlockDiag$Account@1b2c6ec2,持有者 T1
```
改用 ReentrantLock.tryLock 的版本(获取第二把锁最多等待 10 ms,失败则释放第一把锁,随机等待 1~4 ms 后重试),各转账 10 万次,运行 3 次:
```java
static void transferTry(Account from, Account to, int amount) throws InterruptedException {
while (true) {
if (from.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
if (to.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try { from.balance -= amount; to.balance += amount; return; }
finally { to.lock.unlock(); }
}
} finally { from.lock.unlock(); }
}
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 5));
}
}
```
```text
tryLock 版本:完成,余额 1000 1000,用时 59 ms
tryLock 版本:完成,余额 1000 1000,用时 29 ms
tryLock 版本:完成,余额 1000 1000,用时 43 ms
```
## 参考资料
- Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014.
- [Deadlocks in InnoDB | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html)
- [InnoDB Locking | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html)
- [The data_locks Table | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html)
- [ThreadMXBean (Java SE 21 & JDK 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ThreadMXBean.html)