- 引言
- 一、什么是死锁?
- 二、死锁的四个必要条件
- 三、经典死锁场景与代码示例
- 四、如何诊断死锁?
- 五、死锁的预防与避免策略
- 六、实际开发中的最佳实践
- 总结与展望
- 互动环节
引言
在多线程编程中,死锁(Deadlock)就像一场交通瘫痪:四辆车同时到达十字路口,每辆车都在等待其他车先通过,结果谁都动不了 。
这种"互相等待"的局面在并发系统中同样致命——线程永久阻塞,程序停滞不前,系统吞吐量降为零。理解死锁、学会避免死锁,是每个Java开发者迈向高级阶段的必修课。本文将带你彻底攻克这个难题!
一、什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,这些线程都将无法推进下去。
简单比喻:
假设你有两个小朋友:小明和小红 。
小明拿着玩具汽车,但想要小红的玩具熊
小红拿着玩具熊,但想要小明的玩具汽车
两人都不愿意先放下自己手中的玩具,于是就僵持住了...这就是死锁!
二、死锁的四个必要条件
死锁的发生必须同时满足以下四个条件,缺一不可:
- 互斥条件:资源不能被共享,只能由一个线程使用
- 占有且等待:线程已经持有至少一个资源,但又等待获取其他线程持有的资源
- 不可抢占:资源只能由持有它的线程主动释放,不能被强制抢占
- 循环等待:存在一个线程-资源的环形链:T1等待T2占有的资源,T2等待T3占有的资源,...,Tn等待T1占有的资源
打破其中任意一个条件,就能预防死锁!
三、经典死锁场景与代码示例
1. 转账死锁案例
这是最经典的死锁场景:两个人同时向对方转账。
public class BankTransferDeadlock {
static class Account {
private String name;
private int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
void debit(int amount) {
balance -= amount;
}
void credit(int amount) {
balance += amount;
}
}
// 转账方法 - 有死锁风险!
public static void transfer(Account from, Account to, int amount) {
synchronized(from) {
System.out.println(Thread.currentThread().getName()
+ " 锁住了 " + from.name);
// 模拟一些操作,增加死锁发生概率
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(to) {
System.out.println(Thread.currentThread().getName()
+ " 锁住了 " + to.name);
if (from.balance >= amount) {
from.debit(amount);
to.credit(amount);
System.out.println("转账成功");
} else {
System.out.println("余额不足");
}
}
}
}
public static void main(String[] args) {
Account accountA = new Account("张三", 1000);
Account accountB = new Account("李四", 1000);
// 线程1:张三向李四转账100
Thread thread1 = new Thread(() -> transfer(accountA, accountB, 100));
// 线程2:李四向张三转账100
Thread thread2 = new Thread(() -> transfer(accountB, accountA, 100));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("张三余额: " + accountA.balance);
System.out.println("李四余额: " + accountB.balance);
}
}
运行结果可能:
Thread-0 锁住了 张三
Thread-1 锁住了 李四
(然后程序就卡在这里了...)
2. 哲学家就餐问题
另一个经典死锁案例:五位哲学家围坐圆桌,每人面前有一碗饭,每两人之间有一根筷子。哲学家需要两根筷子才能吃饭。
public class DiningPhilosophers {
static class Philosopher implements Runnable {
private final Object leftChopstick;
private final Object rightChopstick;
public Philosopher(Object left, Object right) {
this.leftChopstick = left;
this.rightChopstick = right;
}
@Override
public void run() {
try {
while (true) {
// 思考
doAction("思考中...");
synchronized(leftChopstick) {
doAction("拿起左边筷子");
synchronized(rightChopstick) {
// 吃饭
doAction("拿起右边筷子 - 开始吃饭");
doAction("放下右边筷子");
}
doAction("放下左边筷子 - 回归思考");
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) (Math.random() * 10));
}
}
public static void main(String[] args) {
final int PHILOSOPHERS_COUNT = 5;
Object[] chopsticks = new Object[PHILOSOPHERS_COUNT];
for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % PHILOSOPHERS_COUNT];
// 避免死锁的调整:让最后一个哲学家先拿右边筷子
if (i == PHILOSOPHERS_COUNT - 1) {
Thread philosopher = new Thread(
new Philosopher(rightChopstick, leftChopstick),
"哲学家-" + (i + 1)
);
philosopher.start();
} else {
Thread philosopher = new Thread(
new Philosopher(leftChopstick, rightChopstick),
"哲学家-" + (i + 1)
);
philosopher.start();
}
}
}
}
四、如何诊断死锁?
当程序出现"卡死"现象时,如何确认是死锁?
1. 使用jstack工具
# 1. 找到Java进程ID
jps
# 2. 生成线程转储信息
jstack <pid>
# 或者直接使用jcmd
jcmd <pid> Thread.print
查看输出中的死锁信息:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007faa1c007d80 (object 0x000000076ab66d40, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007faa1c007a80 (object 0x000000076ab66d50, a java.lang.Object),
which is held by "Thread-1"
1754

被折叠的 条评论
为什么被折叠?



