java 死锁产生的8种场景
2025-03-19 本文已影响0人
饱饱抓住了灵感
一、总结
Java死锁的本质是资源竞争与不当的同步控制共同作用的结果。通过合理设计资源分配策略、缩小同步范围、使用超时机制以及选择合适的并发工具,可以显著降低死锁风险。在复杂的多线程场景中,建议结合代码审查和工具分析,确保系统的高可用性。
预防措施:
- 避免嵌套锁:减少锁的粒度,确保每个锁只保护必要的代码。
- 使用超时机制:在锁或线程等待时设置超时时间。
- 资源分级:为资源分配全局唯一顺序,避免环形等待。
- 优先级继承:高优先级线程等待低优先级线程时,继承请求方的优先级。
-
使用并发工具类:如
java.util.concurrent包中的Lock、Semaphore、Atomic类。
诊断工具:
-
JVM工具:
jstack分析线程堆栈,查找BLOCKED状态线程。 - IDE插件:如IntelliJ的“Thread Analyzer”或Eclipse的“Java Concurrency Utilities”。
- 日志监控:记录线程状态和锁竞争日志。
二、死锁场景
1. 资源竞争(最常见场景)
场景示例:
// 资源A和资源B
Object lockA = new Object();
Object lockB = new Object();
// 线程1
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1 holds lockA");
synchronized (lockB) { // 试图获取资源B
System.out.println("Thread 1 holds both locks");
}
}
});
// 线程2
new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2 holds lockB");
synchronized (lockA) { // 试图获取资源A
System.out.println("Thread 2 holds both locks");
}
}
});
死锁原因:
-
循环等待:线程1持有
lockA并请求lockB,线程2持有lockB并请求lockA,形成闭环等待。 - 资源不可抢占:锁一旦被占用,其他线程必须等待释放。
解决方案:
- 统一资源获取顺序:所有线程按固定顺序(如先A后B)获取资源。
-
使用超时机制:在
synchronized或Lock中设置超时时间。
2. 同步代码块调用外部方法
场景示例:
public class DeadlockExample {
private final Object lock = new Object();
public void method1() {
synchronized (lock) {
method2(); // 调用外部方法
}
}
public void method2() {
synchronized (lock) { // 可能导致死锁!
System.out.println("Inside method2");
}
}
}
死锁原因:
-
嵌套锁:线程在持有
lock的情况下调用method2(),而method2()内部再次尝试获取同一个锁。 - 锁粒度过大:同步范围包含不必要的代码。
解决方案:
- 缩小同步范围:仅同步必要的代码块。
- 避免嵌套锁:将外部方法的锁与当前锁分离。
3. 哲学家就餐问题(经典案例)
场景说明:
5个哲学家围坐,每人需要同时拿到左右两边的筷子(资源)才能进食。若所有人同时拿起左边筷子,将全部无法进食。
Java实现模拟:
public class DiningPhilosophers {
private final Lock[] forks = new Lock[5];
private final Condition[] conditions = new Condition[5];
public DiningPhilosophers() {
for (int i = 0; i < 5; i++) {
forks[i] = new ReentrantLock();
conditions[i] = forks[i].newCondition();
}
}
public void eat(int philosopher) throws InterruptedException {
int left = philosopher;
int right = (philosopher + 1) % 5;
forks[left].lock();
try {
forks[right].lock(); // 可能死锁!
System.out.println("Philosopher " + philosopher + " is eating");
Thread.sleep(1000);
} finally {
forks[right].unlock();
forks[left].unlock();
}
}
}
死锁原因:
- 资源环形等待:哲学家们按固定顺序(0→1→2→3→4→0)请求资源。
- 无超时机制:线程无限期等待资源释放。
解决方案:
-
引入超时:在
lock()方法中设置超时时间。 - 资源分级:为资源分配优先级,打破环形等待(如奇数号哲学家先拿左边,偶数号先拿右边)。
4. 线程池配置不当
场景示例:
ExecutorService executor = Executors.newFixedThreadPool(2); // 线程池大小为2
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(10); // 长时间任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
死锁风险:
-
线程阻塞:线程池满载后,新任务因无法获取线程而阻塞,但Java的
ThreadPoolExecutor会将阻塞任务转为RejectedExecutionHandler处理,不会直接死锁。 - 真实死锁场景:若任务内部存在同步代码且资源竞争,可能引发死锁。
解决方案:
- 合理设置线程池大小:根据系统资源调整线程池容量。
-
使用
ThreadPoolExecutor的CallerRunsPolicy:让提交任务的线程自己执行任务,防止阻塞。
5. 并发集合操作不当
场景示例:
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
// 错误写法:在迭代时修改集合
new Thread(() -> {
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getKey() == 1) {
map.put(2, 3); // 并发修改!
}
}
});
// 正确写法:使用迭代器的安全方法
new Thread(() -> {
map.forEach((k, v) -> {
// 仅读操作,安全
});
});
死锁原因:
-
并发修改异常:虽然不会直接导致死锁,但会抛出
ConcurrentModificationException,导致线程中断。 -
锁竞争:若使用
Collections.synchronizedMap,迭代时需额外加锁,可能引发死锁。
解决方案:
-
使用并发集合的安全迭代方法:如
ConcurrentHashMap.forEach()。 -
避免在迭代时修改集合:使用临时副本或
CopyOnWriteArrayList。
6. 数据库连接池泄漏
场景示例:
// 未释放连接的代码
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM table");
// 忘记关闭资源
死锁风险:
- 连接耗尽:线程反复获取连接但未释放,后续请求因无法获取连接而阻塞。
- 隐式死锁:数据库连接池内部可能使用锁管理连接,耗尽时导致线程阻塞。
解决方案:
-
使用
try-with-resources:自动释放资源。 - 配置连接池回收机制:设置连接超时和最大空闲时间。
7. 信号量与条件变量误用
场景示例:
Semaphore semaphore = new Semaphore(1);
Condition condition = new Condition();
public void producer() throws InterruptedException {
semaphore.acquire();
// 生产数据
condition.signalAll();
}
public void consumer() throws InterruptedException {
semaphore.acquire();
condition.await(); // 可能死锁!
// 消费数据
}
死锁原因:
-
等待永不唤醒:若生产者未调用
signalAll(),消费者会无限期等待。 - 优先级反转:高优先级线程等待低优先级线程释放资源。
解决方案:
- 确保信号通知:生产者和消费者必须成对出现(生产后必通知,消费前必等待)。
-
使用
Lock替代synchronized:结合Condition更灵活控制等待/通知。
8. 网络IO阻塞
场景示例:
// 未设置超时的HTTP请求
URL url = new URL("http://example.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
InputStream in = connection.getInputStream();
死锁风险:
- 线程阻塞:网络请求耗时过长,线程无法释放锁或资源。
- 客户端超时:若未设置连接/读取超时,线程可能一直等待。
解决方案:
- 设置超时
connection.setConnectTimeout(5000); // 5秒连接超时
connection.setReadTimeout(5000); // 5秒读取超时