Java 死锁是多线程编程中一种经典且棘手的问题,它会导致多个线程相互等待对方持有的资源而永久阻塞。理解其产生原因和预防措施至关重要。
死锁的发生需要同时满足以下四个必要条件(缺一不可):
互斥使用 (Mutual Exclusion):
synchronized
关键字或 Lock
对象实现的锁机制本质上就提供了这种互斥性。持有并等待 (Hold and Wait / Partial Allocation):
不可剥夺 (No Preemption):
synchronized
锁不能被强制中断释放;Lock.lock()
获取的锁也不能被其他线程强制解锁(除非使用 Lock.lockInterruptibly()
并中断线程,但这通常也不是“强行剥夺”的含义)。循环等待 (Circular Wait):
{T1, T2, ..., Tn}
,其中:
public class DeadlockExample {
static final Object lockA = new Object();
static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) { // 线程1获取lockA
System.out.println("Thread1 acquired lockA");
try {
Thread.sleep(100); // 模拟操作,增加死锁发生概率
} catch (InterruptedException e) {}
synchronized (lockB) { // 线程1尝试获取lockB(此时可能被线程2持有)
System.out.println("Thread1 acquired lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) { // 线程2获取lockB
System.out.println("Thread2 acquired lockB");
try {
Thread.sleep(100); // 模拟操作,增加死锁发生概率
} catch (InterruptedException e) {}
synchronized (lockA) { // 线程2尝试获取lockA(此时被线程1持有)
System.out.println("Thread2 acquired lockA");
}
}
});
thread1.start();
thread2.start();
}
}
lockA
和 lockB
都是 synchronized
使用的对象,具有互斥性。lockA
,同时等待获取 lockB
。lockB
,同时等待获取 lockA
。synchronized
锁不能被其他线程强行剥夺。lockB
。lockA
。防止死锁的核心策略就是破坏上述四个必要条件中的至少一个。以下是常用的方法:
hashCode
、按一个预定义的唯一ID、按名称排序等)。Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 总是先A后B
System.out.println("Thread1 acquired lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockA) { // 线程2也先尝试获取lockA
System.out.println("Thread2 acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 再获取lockB
System.out.println("Thread2 acquired lockB");
}
}
});
System.identityHashCode(Object)
作为最后手段来排序,但要注意哈希冲突。tryLock
)。Lock
接口(特别是 ReentrantLock
)提供了 tryLock()
方法(可带超时)来实现这种细粒度控制,这比 synchronized
更灵活。ReentrantLock
和 tryLock
):import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockPrevention {
static Lock lockA = new ReentrantLock();
static Lock lockB = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> acquireLocksAndWork(lockA, lockB, "Thread1"));
Thread thread2 = new Thread(() -> acquireLocksAndWork(lockB, lockA, "Thread2")); // 注意顺序不同,但方法内部处理
thread1.start();
thread2.start();
}
public static void acquireLocksAndWork(Lock firstLock, Lock secondLock, String threadName) {
while (true) {
boolean gotFirst = false;
boolean gotSecond = false;
try {
// 尝试获取第一个锁(带超时避免无限等待)
gotFirst = firstLock.tryLock(100, TimeUnit.MILLISECONDS);
if (gotFirst) {
System.out.println(threadName + " acquired first lock");
// 尝试获取第二个锁(带超时)
gotSecond = secondLock.tryLock(100, TimeUnit.MILLISECONDS);
if (gotSecond) {
System.out.println(threadName + " acquired second lock");
// 成功获取两个锁,执行工作
System.out.println(threadName + " doing work...");
Thread.sleep(500); // 模拟工作
break; // 工作完成,跳出循环
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 无论如何,在退出前确保释放已获得的锁
if (gotSecond) secondLock.unlock();
if (gotFirst) firstLock.unlock();
}
// 如果没能一次性获得两个锁,等待随机时间后重试,避免活锁
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {}
}
}
}
synchronized
块的范围,只保护真正需要互斥访问的共享数据操作。不要在锁内执行耗时操作(如IO)。ConcurrentHashMap
, CopyOnWriteArrayList
, AtomicInteger
等并发容器和原子类,它们内部实现了高效的并发控制,减少了你显式加锁的需要。final
字段,构造后状态不变)。访问不可变对象不需要同步。ThreadLocal
为每个线程创建变量的副本,避免共享。Lock
接口的 tryLock(long time, TimeUnit unit)
方法。synchronized
无法直接实现超时。tryLock
带超时。ThreadMXBean
的 findDeadlockedThreads()
或 findMonitorDeadlockedThreads()
方法来检测由 synchronized
或 ownable synchronizers
(如 ReentrantLock
) 引起的死锁。JMX 工具(如 JConsole, VisualVM)通常集成了这个功能。Thread.stop()
) 是极其危险且已被废弃的方法,会导致数据不一致等严重问题,绝对不要使用。Lock
和 tryLock
: 当锁顺序难以严格保证或需要更灵活控制时,使用 ReentrantLock
及其 tryLock
(带超时)方法,实现一次性申请所有锁或锁超时机制。务必在 finally
块中释放锁。synchronized
块)。java.util.concurrent.*
) 和原子变量。ThreadLocal
)。Condition.await
、线程 join
、Future.get
等)使用超时参数,防止永久阻塞,给系统提供回退的机会。jstack
命令行工具等定期检查或在线诊断潜在的死锁。jstack -l
输出的线程转储会明确标识出找到的死锁和涉及的线程/锁。记住: 预防死锁的关键在于设计和编码阶段就意识到风险并应用上述策略。事后检测和恢复往往是代价高昂的最后手段。