在多线程编程中,死锁(Deadlock)是一个常见且棘手的问题。当多个线程互相等待对方释放资源,而没有一个线程能够继续执行时,就会发生死锁,导致所有相关线程被阻塞,程序失去响应。本文将深入探讨死锁的原理、产生条件、诊断方法以及解决方案。
什么是死锁?
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干预,这些线程都将无法向前推进。
死锁的经典示例
让我们通过一个简单的Java代码示例来演示死锁:
public class DeadlockExample { private static final Object resource1 = new Object(); private static final Object resource2 = new Object(); public static void main(String[] args) { // 线程1:先锁resource1,再锁resource2 Thread thread1 = new Thread(() -> { synchronized (resource1) { System.out.println("线程1:锁住了resource1"); try { Thread.sleep(100); // 模拟一些操作 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource2) { System.out.println("线程1:锁住了resource2"); } } }); // 线程2:先锁resource2,再锁resource1 Thread thread2 = new Thread(() -> { synchronized (resource2) { System.out.println("线程2:锁住了resource2"); try { Thread.sleep(100); // 模拟一些操作 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource1) { System.out.println("线程2:锁住了resource1"); } } }); thread1.start(); thread2.start(); } }
运行这段代码,很可能出现以下输出:
线程1:锁住了resource1 线程2:锁住了resource2
然后程序就会卡住,两个线程都无法继续执行。
死锁产生的四个必要条件
死锁的发生必须同时满足以下四个条件:
1. 互斥条件
资源在同一时刻只能被一个线程占用。如果资源可以被多个线程共享,就不会发生死锁。
2. 请求与保持条件
线程已经保持至少一个资源,同时又请求其他资源,而该资源被其他线程占用,导致该线程阻塞,但又不释放自己已持有的资源。
3. 不剥夺条件
线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只能由自己主动释放。
4. 循环等待条件
在发生死锁时,必然存在一个线程-资源的环形链。即线程集合{T0, T1, T2, …, Tn}中,T0等待T1占用的资源,T1等待T2占用的资源,…,Tn等待T0占用的资源。
如何诊断死锁
1. 使用jstack工具
对于Java应用,JDK自带的jstack工具是诊断死锁的有力工具:
# 首先找到Java进程的PID jps -l # 使用jstack查看线程堆栈 jstack <PID>
当存在死锁时,jstack的输出中会明确提示:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8b7c006b00 (object 0x00000007d6a2c9a0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8b7c008b00 (object 0x00000007d6a2c9b0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockExample.lambda$main$1(DeadlockExample.java:28)
- waiting to lock <0x00000007d6a2c9a0> (a java.lang.Object)
- locked <0x00000007d6a2c9b0> (a java.lang.Object)
at DeadlockExample$$Lambda$2/1831932724.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at DeadlockExample.lambda$main$0(DeadlockExample.java:14)
- waiting to lock <0x00000007d6a2c9b0> (a java.lang.Object)
- locked <0x00000007d6a2c9a0> (a java.lang.Object)
at DeadlockExample$$Lambda$1/558638686.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
2. 使用JConsole图形化工具
JConsole是JDK自带的图形化监控工具,可以直观地检测死锁:
-
运行
jconsole命令启动 -
连接到目标Java进程
-
点击”线程”选项卡
-
点击”检测死锁”按钮
JConsole会高亮显示死锁的线程,并展示它们等待的资源关系。
3. 通过代码检测
也可以在代码中主动检测死锁:
import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; public class DeadlockDetector { public static void checkDeadlocks() { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); if (deadlockedThreads != null && deadlockedThreads.length > 0) { System.out.println("检测到死锁!涉及以下线程:"); ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads); for (ThreadInfo threadInfo : threadInfos) { System.out.println("线程: " + threadInfo.getThreadName()); System.out.println("状态: " + threadInfo.getThreadState()); System.out.println("等待的锁: " + threadInfo.getLockName()); System.out.println("持有的锁: " + threadInfo.getLockOwnerName()); System.out.println("---"); } } else { System.out.println("未检测到死锁"); } } }
死锁的预防策略
1. 破坏互斥条件
-
使用无锁编程技术,如CAS(Compare and Swap)
-
使用线程安全的并发容器,如ConcurrentHashMap
-
但有些资源天生就是互斥的,无法完全破坏
2. 破坏请求与保持条件
-
一次性申请所有资源:要求线程在开始执行前,一次性申请所有需要的资源
public class SafeAllocation { // 使用一个锁来保护资源分配 private static final Object allocationLock = new Object(); public void performOperation() { synchronized (allocationLock) { // 同时获取所有需要的资源 synchronized (resource1) { synchronized (resource2) { // 执行操作 } } } } }
3. 破坏不剥夺条件
-
如果线程申请不到所有资源,就释放已经持有的资源
-
使用tryLock操作,设置超时时间
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class TryLockExample { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock(); public void performOperation() { while (true) { try { // 尝试获取lock1,最多等待100ms if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 尝试获取lock2,最多等待100ms if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 成功获取两个锁,执行操作 System.out.println("成功获取两个锁,执行操作"); return; } finally { lock2.unlock(); } } } finally { // 如果没有获取到lock2,释放lock1 lock1.unlock(); } } // 没有获取到所有锁,等待一段时间后重试 Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }
4. 破坏循环等待条件
-
锁排序:规定所有线程必须按照相同的顺序获取锁
public class LockOrdering { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); // 定义锁的获取顺序,总是先获取lock1,再获取lock2 public void method1() { synchronized (lock1) { synchronized (lock2) { // 执行操作 } } } public void method2() { synchronized (lock1) { // 先获取lock1 synchronized (lock2) { // 再获取lock2 // 执行操作 } } } }
死锁的避免策略
1. 使用定时锁
通过tryLock的超时机制,避免线程无限期等待:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class TimeoutLockExample { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock(); public boolean transfer() { boolean lock1Acquired = false; boolean lock2Acquired = false; try { lock1Acquired = lock1.tryLock(1, TimeUnit.SECONDS); if (!lock1Acquired) { return false; } // 模拟一些操作 Thread.sleep(100); lock2Acquired = lock2.tryLock(1, TimeUnit.SECONDS); if (!lock2Acquired) { return false; } // 执行实际的操作 System.out.println("操作成功执行"); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { if (lock2Acquired) { lock2.unlock(); } if (lock1Acquired) { lock1.unlock(); } } } }
2. 使用并发工具类
Java提供了许多高级并发工具,可以有效避免死锁:
-
Semaphore:信号量,控制同时访问资源的线程数
-
CountDownLatch:允许一个或多个线程等待其他线程完成操作
-
CyclicBarrier:允许多个线程互相等待,直到到达某个公共屏障点
-
BlockingQueue:线程安全的队列,可以避免显式的锁操作
import java.util.concurrent.Semaphore; public class SemaphoreExample { private final Semaphore semaphore1 = new Semaphore(1); private final Semaphore semaphore2 = new Semaphore(1); public void safeMethod() { try { semaphore1.acquire(); semaphore2.acquire(); // 执行操作 System.out.println("操作执行中..."); semaphore2.release(); semaphore1.release(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
实际案例分析
案例:银行转账系统
一个典型的银行转账系统,如果没有正确处理锁,很容易发生死锁:
public class BankAccount { private final int id; private double balance; private final Lock lock = new ReentrantLock(); public BankAccount(int id, double balance) { this.id = id; this.balance = balance; } // 有死锁风险的转账方法 public void transferDeadlock(BankAccount target, double amount) { // 先锁住当前账户 lock.lock(); try { System.out.println(Thread.currentThread().getName() + " 锁住了账户 " + id); // 模拟一些操作,增加死锁概率 Thread.sleep(100); // 再锁住目标账户 target.lock.lock(); try { System.out.println(Thread.currentThread().getName() + " 锁住了账户 " + target.id); if (balance >= amount) { balance -= amount; target.balance += amount; System.out.println("转账成功: " + amount + " 从账户 " + id + " 到 " + target.id); } } finally { target.lock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } // 正确的转账方法:按照账户ID顺序获取锁 public void transferSafe(BankAccount target, double amount) { BankAccount firstLock = this.id < target.id ? this : target; BankAccount secondLock = this.id < target.id ? target : this; firstLock.lock.lock(); try { System.out.println(Thread.currentThread().getName() + " 锁住了账户 " + firstLock.id); Thread.sleep(100); secondLock.lock.lock(); try { System.out.println(Thread.currentThread().getName() + " 锁住了账户 " + secondLock.id); if (this.balance >= amount) { this.balance -= amount; target.balance += amount; System.out.println("转账成功: " + amount + " 从账户 " + this.id + " 到 " + target.id); } } finally { secondLock.lock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { firstLock.lock.unlock(); } } }
总结
死锁是多线程编程中一个严重的问题,会导致线程全部阻塞,系统失去响应。要有效预防和处理死锁,我们需要:
-
理解死锁的本质:四个必要条件同时满足
-
掌握诊断工具:jstack、JConsole等
-
采取预防策略:破坏四个必要条件中的任意一个
-
使用高级并发工具:利用Java提供的并发类库
-
遵循最佳实践:锁排序、超时机制等
在实际开发中,我们应该在设计阶段就考虑死锁的预防,并在测试阶段充分测试并发场景。同时,保持代码简单清晰,避免复杂的嵌套锁结构,也能大大降低死锁的风险。
记住:预防胜于治疗,在编写多线程代码时,始终要警惕死锁的可能性!