死锁是多线程并发中因资源竞争导致的永久阻塞状态,其产生需满足四个条件:
互斥持有资源、
持有并等待新资源、
资源不可剥夺、
循环等待资源。
避免死锁的核心是打破这四个条件中的至少一个。
针对 synchronized 和 ReentrantLock 的特性,可采用以下具体策略:
无论使用哪种锁,以下原则能从根本上减少死锁风险:
死锁最常见的场景是 “线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1”(循环等待)。让所有线程按统一顺序获取锁,可彻底避免循环等待。
示例: 定义锁的 “优先级”(如按锁对象的哈希值或业务逻辑排序),所有线程必须先获取低优先级锁,再获取高优先级锁。
// 锁1和锁2,规定必须先获取lock1,再获取lock2
Object lock1 = new Object();
Object lock2 = new Object();
// 线程A:按顺序获取lock1→lock2
new Thread(() -> {
synchronized (lock1) { // 先低优先级
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) { // 再高优先级
System.out.println("线程A获取双锁成功");
}
}
}).start();
// 线程B:同样按顺序获取lock1→lock2(而非lock2→lock1)
new Thread(() -> {
synchronized (lock1) { // 先低优先级
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) { // 再高优先级
System.out.println("线程B获取双锁成功");
}
}
}).start();
原理:所有线程获取锁的顺序一致,不会出现 “交叉等待”,打破循环等待条件。
嵌套锁是死锁的高危场景(持有锁 A 时等待锁 B,持有锁 B 时可能等待其他锁)。尽量避免锁的嵌套,或减少嵌套层数,可降低死锁风险。
反例(危险):
// 嵌套锁导致死锁风险
synchronized (lockA) {
// 业务逻辑
synchronized (lockB) {
// 嵌套逻辑
}
}
优化: 将嵌套逻辑拆分为独立方法,通过 “一次性获取所有需要的锁” 或 “减少锁的范围” 避免嵌套。
线程持有锁的时间越长,其他线程等待锁的时间就越长,死锁概率越高。在锁内部只执行核心逻辑,快速释放锁,可减少冲突窗口。
示例:
synchronized (lock) {
// 只做必须同步的操作(如修改共享变量)
sharedVar = newValue;
}
// 非核心逻辑(如IO、计算)放在锁外部
nonCriticalOperation();
若线程获取锁后因异常退出而未释放锁,会导致其他线程永久等待(资源被永久持有)。需确保锁最终被释放:
synchronized 由 JVM 自动释放(无需手动处理)。ReentrantLock 必须在 finally 中调用 unlock() 释放。ReentrantLock 提供的 超时机制 和 中断支持 是避免死锁的强大工具,可主动打破 “持有并等待” 和 “不可剥夺” 条件。
通过 tryLock(long timeout, TimeUnit unit) 尝试获取锁,若超时未获取则主动释放已持有的锁,避免永久阻塞。
示例: 线程需要获取锁 A 和锁 B,若获取锁 B 超时,则释放已持有的锁 A:
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
new Thread(() -> {
boolean gotA = false;
boolean gotB = false;
try {
// 先获取锁A
gotA = lockA.tryLock(1, TimeUnit.SECONDS);
if (!gotA) {
System.out.println("线程1获取锁A失败,放弃");
return;
}
// 再尝试获取锁B,超时1秒
gotB = lockB.tryLock(1, TimeUnit.SECONDS);
if (!gotB) {
System.out.println("线程1获取锁B超时,释放锁A");
return; // 未获取锁B,自动释放已持有的锁A(在finally中)
}
// 成功获取双锁,执行逻辑
System.out.println("线程1获取双锁成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (gotB) lockB.unlock(); // 释放锁B
if (gotA) lockA.unlock(); // 释放锁A
}
}).start();
原理:超时后主动放弃,避免 “持有锁 A 并永久等待锁 B”,打破 “持有并等待” 条件。
通过 lockInterruptibly() 允许等待锁的线程被中断(如其他线程调用 interrupt()),从而退出等待,避免死锁。
示例:
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 可中断地获取锁:若线程被中断,会抛出InterruptedException
lock.lockInterruptibly();
try {
// 执行逻辑(若长时间运行,外部可中断)
Thread.sleep(10000);
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程1获取锁时被中断,退出");
}
});
t1.start();
// 若t1长时间未释放锁,主线程可中断它
new Thread(() -> {
try {
Thread.sleep(1000);
t1.interrupt(); // 中断t1,使其退出等待
} catch (InterruptedException e) {}
}).start();
原理:线程可被外部中断,打破 “不可剥夺” 条件(无需等待锁释放,主动退出)。
通过 tryLock()(无参)尝试获取锁,若当前锁被占用则立即返回 false,可实现 “非阻塞” 逻辑,避免进入等待状态。
示例:
if (lock.tryLock()) { // 若锁未被占用,立即获取
try {
// 执行逻辑
} finally {
lock.unlock();
}
} else {
// 锁被占用,执行备选逻辑(如重试、降级处理)
}
synchronized 无超时、中断机制,需更依赖设计层面的规避:
因 synchronized 无法主动放弃等待,固定锁的获取顺序是避免死锁的核心(如前文 “通用策略 1”)。
外部方法可能隐藏未知的锁操作(如调用其他类的同步方法),导致意外的锁嵌套和死锁。
反例:
synchronized (lockA) {
// 调用外部方法,可能内部持有lockB,导致锁顺序混乱
externalService.doSomething();
}
优化: 提前确认外部方法是否涉及同步操作,或通过接口定义约束锁的使用。
若多个线程竞争的资源(锁)数量越少,死锁概率越低。尽量合并或减少共享资源,降低锁的竞争面。
若死锁已发生,可通过工具检测并恢复:
BLOCKED 状态的线程及等待的锁,定位死锁。shutdownNow() 中断所有线程(需线程支持中断)。避免死锁的核心思路是打破死锁的四个条件,具体措施因锁的类型而异:
tryLock(timeout) 超时机制、lockInterruptibly() 中断支持,主动避免永久等待。实际开发中,预防优于补救,需在设计阶段就通过合理的锁策略规避死锁风险。