0%

线程死锁

线程死锁:原理、成因与解决方案

线程死锁是多线程编程中常见的隐性问题,指两个或多个线程相互等待对方持有的资源,导致所有线程陷入无限期阻塞的状态。死锁一旦发生,程序往往无法自动恢复,需通过代码设计规避或手动干预解决。

死锁的核心成因

死锁的产生必须同时满足以下四个必要条件,缺一不可:

  1. 互斥条件
    资源具有排他性,同一时间只能被一个线程占用(如synchronized锁、文件句柄等)。
  2. 请求与保持条件
    线程持有部分资源,同时又请求其他线程已持有的资源,且不释放自身已占有的资源。
  3. 不剥夺条件
    线程已获得的资源,在未主动释放前,不能被其他线程强行剥夺(如 Java 的内置锁无法被强制释放)。
  4. 循环等待条件
    多个线程形成环形等待链,每个线程都在等待下一个线程持有的资源(如线程 A 等线程 B 的资源,线程 B 等线程 A 的资源)。

死锁示例代码

以下是一个典型的死锁场景:两个线程分别持有对方需要的锁,且都不释放自身的锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class DeadlockExample {
// 定义两个共享资源(锁)
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();

public static void main(String[] args) {
// 线程1:持有LOCK_A,请求LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("线程1获得LOCK_A,尝试获取LOCK_B...");
try {
Thread.sleep(100); // 模拟业务操作,放大死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println("线程1获得LOCK_B,执行完成");
}
}
}, "线程1").start();

// 线程2:持有LOCK_B,请求LOCK_A
new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("线程2获得LOCK_B,尝试获取LOCK_A...");
try {
Thread.sleep(100); // 模拟业务操作,放大死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_A) {
System.out.println("线程2获得LOCK_A,执行完成");
}
}
}, "线程2").start();
}
}

执行结果

1
2
线程1获得LOCK_A,尝试获取LOCK_B...
线程2获得LOCK_B,尝试获取LOCK_A...

程序卡死,两个线程永远无法继续执行。

死锁的检测与定位

在开发和调试阶段,可通过以下工具检测死锁:

JDK 自带工具

  • jps:查看 Java 进程 ID

    1
    jps -l  # 输出进程ID和主类名
  • jstack:打印线程堆栈,检测死锁

    1
    jstack <进程ID>  # 若存在死锁,会显示"Found one Java-level deadlock"

    死锁部分堆栈示例:

    1
    2
    3
    4
    5
    6
    7
    8
    Found one Java-level deadlock:
    =============================
    "线程2":
    waiting to lock monitor 0x00007f8a3a00a888 (object 0x000000076b618610, a java.lang.Object),
    which is held by "线程1"
    "线程1":
    waiting to lock monitor 0x00007f8a3a00d088 (object 0x000000076b618620, a java.lang.Object),
    which is held by "线程2"

代码层面检测

通过ThreadMXBean实时监控死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 定时检测死锁
new Thread(() -> {
while (true) {
long[] deadlockedThreadIds = threadMXBean.findDeadlockedThreads();
if (deadlockedThreadIds != null) {
System.out.println("检测到死锁,线程ID:" + Arrays.toString(deadlockedThreadIds));
// 可进一步打印堆栈信息或触发告警
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

死锁的预防与解决

解决死锁的核心思路是破坏四个必要条件中的至少一个,具体方案如下:

1.破坏 “循环等待条件”(最常用)

方案:为所有资源定义统一的获取顺序,线程必须按顺序获取资源,避免环形等待。

优化示例(修复上述死锁代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 规定获取顺序:先获取LOCK_A,再获取LOCK_B
public class FixedDeadlockExample {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();

public static void main(String[] args) {
// 线程1:按顺序获取LOCK_A → LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("线程1获得LOCK_A,尝试获取LOCK_B...");
synchronized (LOCK_B) {
System.out.println("线程1获得LOCK_B,执行完成");
}
}
}, "线程1").start();

// 线程2:同样按顺序获取LOCK_A → LOCK_B(而非先LOCK_B)
new Thread(() -> {
synchronized (LOCK_A) { // 修复点:先获取LOCK_A
System.out.println("线程2获得LOCK_A,尝试获取LOCK_B...");
synchronized (LOCK_B) {
System.out.println("线程2获得LOCK_B,执行完成");
}
}
}, "线程2").start();
}
}

2.破坏 “请求与保持条件”

方案:线程获取资源前,先释放已持有的所有资源;或一次性申请所有所需资源,未获取全部则不持有任何资源。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 一次性申请所有资源,失败则释放已获资源
public class NoHoldLockExample {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();

// 尝试同时获取两个锁
private static boolean tryAcquireBothLocks() {
synchronized (LOCK_A) {
if (Thread.holdsLock(LOCK_B)) { // 若已持有LOCK_B,直接返回
return true;
}
synchronized (LOCK_B) {
return true;
}
}
}

public static void main(String[] args) {
new Thread(() -> {
if (tryAcquireBothLocks()) {
System.out.println("线程1获取所有锁,执行完成");
}
}).start();
}
}

3.破坏 “不剥夺条件”

方案:使用可中断的锁(如ReentrantLock),允许线程在超时或被中断时释放已持有的锁。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockExample {
private static final Lock LOCK_A = new ReentrantLock();
private static final Lock LOCK_B = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
if (LOCK_A.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取LOCK_A,超时1秒
try {
if (LOCK_B.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取LOCK_B,超时1秒
try {
System.out.println("线程1获取所有锁,执行完成");
} finally {
LOCK_B.unlock();
}
} else {
System.out.println("线程1获取LOCK_B超时,释放LOCK_A");
}
} finally {
LOCK_A.unlock();
}
}
} catch (InterruptedException e) {
System.out.println("线程1被中断,释放资源");
}
});

t1.start();
// 其他线程逻辑...
}
}

4. 破坏 “互斥条件”

方案:使用可共享的资源(如ReadWriteLock的读锁),允许多个线程同时访问资源。但该方案仅适用于读多写少的场景,不适用于排他性资源。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10

域名更新通知

您好!我们的官方域名已更新为 zhhll.com.cn。 请收藏新域名以获取最佳访问体验。