一、死锁:程序员的隐形噩梦
在多线程编程中,死锁如同交通堵塞中的连环追尾事故,一旦发生就会导致程序完全停滞。当两个或多个线程因争夺资源而陷入无限等待时,系统就像被按下了暂停键,既无法前进也不能后退。
二、死锁经典场景
1. 不可重入锁陷阱(理论场景)
// Java中synchronized是可重入的,此示例仅作理论说明
public class FakeDeadlock {
public synchronized void methodA() {
methodB(); // 如果锁不可重入,此处将死锁
}
public synchronized void methodB() {
// 方法实现
}
}
2. 双线程资源争夺战
public class DiningLock {
static final Object CHOPSTICKS = new Object();
static final Object SPOON = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (CHOPSTICKS) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (SPOON) {
System.out.println("小美开始用餐");
}
}
}, "小美").start();
new Thread(() -> {
synchronized (SPOON) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (CHOPSTICKS) {
System.out.println("小帅开始用餐");
}
}
}, "小帅").start();
}
}
三、死锁四大必要条件
必要条件 | 说明 | 是否可规避 |
---|---|---|
互斥使用 | 资源独占性 | ❌ 系统特性 |
不可抢占 | 资源不可强占 | ❌ 系统特性 |
请求保持 | 持有资源申请新资源 | ❌ 编程特性 |
循环等待 | 资源申请形成闭环 | ✅ 可破解 |
四、破解死锁的黄金法则
1. 统一资源获取顺序
public class SafeDining {
static final Object CHOPSTICKS = new Object();
static final Object SPOON = new Object();
// 定义全局资源获取顺序
static final List<Object> LOCK_ORDER =
Collections.unmodifiableList(Arrays.asList(SPOON, CHOPSTICKS));
public static void main(String[] args) {
new Thread(() -> acquireLocks(LOCK_ORDER), "小美").start();
new Thread(() -> acquireLocks(LOCK_ORDER), "小帅").start();
}
static void acquireLocks(List<Object> locks) {
synchronized (locks.get(0)) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (locks.get(1)) {
System.out.println(Thread.currentThread().getName() + "开始用餐");
}
}
}
}
2. 哲学家就餐问题解决方案
class Philosopher implements Runnable {
private final int id;
private final Object leftChopstick;
private final Object rightChopstick;
public Philosopher(int id, Object[] chopsticks) {
this.id = id;
// 通过编号控制获取顺序
int first = id % 2 == 0 ? id : (id + 1) % 5;
int second = id % 2 == 0 ? (id + 1) % 5 : id;
this.leftChopstick = chopsticks[Math.min(first, second)];
this.rightChopstick = chopsticks[Math.max(first, second)];
}
public void run() {
while (true) {
synchronized(leftChopstick) {
synchronized(rightChopstick) {
System.out.println("哲学家" + id + "用餐中");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
}
五、死锁检测与预防工具箱
1. 诊断工具
-
jstack:查看线程堆栈信息
-
VisualVM:图形化线程分析
-
Arthas:阿里开源的诊断神器
2. 防御性编程技巧
// 使用tryLock避免无限等待
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区代码
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
六、死锁预防策略对比
策略 | 实现难度 | 性能影响 | 适用场景 |
---|---|---|---|
资源排序法 | ★★☆ | 低 | 固定资源需求 |
超时机制 | ★★★ | 中 | 实时性要求低 |
资源预分配 | ★★★★ | 高 | 关键系统 |
检测恢复 | ★★★★★ | 不定 | 复杂系统 |
七、最佳实践指南
-
最小化锁范围
// 不推荐 synchronized(this) { // 大量非共享操作 sharedResource++; } // 推荐 // 非共享操作... synchronized(this) { sharedResource++; }
-
避免嵌套锁
public void dangerMethod() { synchronized(lockA) { // 不要在此处调用其他同步方法 anotherSyncMethod(); // 风险点! } }
-
使用并发工具类
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>(); AtomicInteger counter = new AtomicInteger();
八、总结:构建无死锁系统
关键要点:
-
死锁预防胜于治疗
-
资源排序是最有效解决方案
-
定期使用诊断工具扫描
-
在系统设计阶段考虑并发安全
通过理解死锁的本质,遵循规范的编程实践,并善用现代并发工具,开发者可以构建出既高效又安全的多线程系统。记住:良好的设计是预防死锁的第一道防线!