- 引言
- 一、什么是死锁?
- 二、死锁的四个必要条件
- 三、经典死锁场景与代码示例
- 四、如何诊断死锁?
- 五、死锁的预防与避免策略
- 六、实际开发中的最佳实践
- 总结与展望
- 互动环节
引言
在多线程编程中,死锁(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"