死锁是Java并发编程中多个线程因循环等待资源而陷入的永久阻塞状态。文章详细分析了8种常见死锁场景及解决方案:1. 经典资源顺序死锁,通过统一锁获取顺序避免;2. 多资源有序死锁,采用全局资源编号并按序获取;3. 数据库死锁,确保事务访问表顺序一致并缩短持有锁时间;4. 嵌套同步块死锁,保持嵌套锁获取顺序一致;5. 外部方法回调死锁,避免持锁时调用外部方法,使用tryLock或细粒度锁;6. 线程池任务提交死锁,合理配置线程池或分离任务队列;7. JMX/RMI远程调用死锁,采用异步通信与超时机制;8. CountDownLatch或CyclicBarrier误用,设计合理等待条件并设置超时。此外,文章介绍了使用jstack、JConsole等工具检测死锁的方法,并指出并发编程中还需警惕饥饿、活锁、可见性、原子性、有序性和线程安全等问题。在微服务架构下,传统死锁解决方案仍适用于服务内部,但需结合Saga模式、消息队列、分布式锁和熔断限流等机制应对分布式死锁风险。

在Java并发编程的复杂世界里,死锁就像是一个隐匿的陷阱,一旦触发,整个系统就可能陷入停滞,响应全无。它本质上是多个线程相互等待对方释放资源,从而形成一个循环依赖,谁也无法继续执行的僵局。理解这些常见的死锁场景并掌握规避策略,是每个Java开发者确保系统稳定性和响应性的关键一步。这不仅仅是理论知识,更是我们日常调试和系统设计中不得不面对的真实挑战。
死锁并非无法避免,关键在于我们如何设计和管理共享资源的访问。以下是8种常见的死锁场景及其对应的解决方案:
1. 经典资源顺序死锁 (A等待B,B等待A)
这是最常见也最容易理解的死锁形式。两个或多个线程各自持有一个资源,并尝试获取对方持有的另一个资源。
立即学习“Java免费学习笔记(深入)”;
场景描述: 线程1持有资源A,尝试获取资源B;同时,线程2持有资源B,尝试获取资源A。
示例:
Object lockA = new Object();
Object lockB = new Object();
// Thread 1
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1: Holding lockA...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lockB...");
synchronized (lockB) {
System.out.println("Thread 1: Holding lockA & lockB.");
}
}
}).start();
// Thread 2
new Thread(() -> {
synchronized (lockB) { // 注意这里,如果先获取lockB
System.out.println("Thread 2: Holding lockB...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lockA...");
synchronized (lockA) {
System.out.println("Thread 2: Holding lockB & lockA.");
}
}
}).start();解决方案: 强制所有线程以相同的顺序获取锁。如果所有线程都先获取
lockA
lockB
2. 多资源有序死锁 (环路等待)
这是经典死锁的扩展,涉及三个或更多资源,形成一个等待环。
3. 数据库死锁 (事务死锁)
当Java应用与数据库交互时,如果多个事务并发执行,且它们更新或锁定资源的顺序不一致,就可能导致数据库层面的死锁。
FOR UPDATE
SQLException
4. 嵌套同步块死锁
当一个线程在一个同步块内部又尝试获取另一个同步块的锁时,如果获取顺序不当,就可能导致死锁。
synchronized(obj1) { synchronized(obj2) { ... } }synchronized(obj2) { synchronized(obj1) { ... } }5. 外部方法回调死锁
当一个线程持有锁A,然后调用一个外部(或可能由第三方库提供的)方法,而这个外部方法又尝试获取锁A,或者获取了锁B后,又回调到当前线程,当前线程又尝试获取锁B,这都可能形成死锁。
service.call()
service.call()
service.call()
ReentrantLock
tryLock()
ReadWriteLock
6. 线程池任务提交死锁
这种死锁相对隐蔽,发生在线程池中,当任务A提交到线程池,而任务A又需要等待任务B完成,而任务B又被提交到同一个已满的线程池中。
CompletableFuture
Future
get(timeout)
7. JMX/RMI 远程调用死锁
在分布式系统中,如果多个服务之间通过JMX或RMI进行同步调用,并形成循环依赖,就可能导致跨进程或跨JVM的死锁。
CompletableFuture
8. CountDownLatch
CyclicBarrier
这些并发工具通常用于协调多个线程的执行,但如果等待条件永远无法满足,或者等待的线程本身就是提供条件的线程,就可能导致死锁。
CountDownLatch
CountDownLatch
CyclicBarrier
await()
死锁的检测和定位,往往比预防来得更紧急,也更考验我们的工程实践能力。毕竟,代码不是一次写成就完美的,线上问题总会不期而至。
首先,最直观的线索是系统响应变慢甚至完全停滞,CPU占用率可能不高,但线程却大量处于
BLOCKED
jstack
jstack -l <pid>
<pid>
jstack
RUNNABLE
BLOCKED
WAITING
jstack
定位死锁的关键在于理解线程快照中的
BLOCKED
waiting for monitor entry
waiting to lock
locked <address>
holding a monitor
Java并发编程的坑远不止死锁一个,它是一个充满挑战的领域。在我看来,除了死锁,还有几个“老面孔”经常让开发者头疼,它们同样会导致系统行为异常、性能下降甚至数据损坏。
volatile
synchronized
ReentrantLock
java.util.concurrent
Atomic
i++
i
i
i
synchronized
ReentrantLock
java.util.concurrent.atomic
AtomicInteger
volatile
synchronized
ReentrantLock
ArrayList
HashMap
ConcurrentModificationException
java.util.concurrent
ConcurrentHashMap
CopyOnWriteArrayList
Collections.synchronizedList()
这些问题往往相互关联,一个系统的并发问题可能由多种陷阱共同导致。因此,在进行并发编程时,我们需要时刻保持警惕,深入理解JVM内存模型和各种并发工具的底层原理。
微服务架构的兴起,确实给传统的并发问题,特别是死锁,带来了新的视角和挑战。在我看来,传统的死锁解决方案在微服务环境中,既有其适用性,也需要结合分布式特性进行扩展和调整。
传统解决方案的适用性:
首先,微服务内部的死锁问题,比如单个服务内部的多个线程争抢同一个数据库连接池的资源,或者服务内部的业务逻辑涉及多个
synchronized
tryLock
微服务架构带来的新挑战与扩展:
微服务架构的特点是服务自治、分布式部署。这意味着死锁可能不再仅仅局限于单个JVM内部,而可能蔓延到跨服务的层面,形成“分布式死锁”。
总而言之,在微服务架构下,我们仍然需要关注服务内部的并发问题,并应用传统的死锁解决方案。但同时,我们也必须将视野扩展到服务间,利用异步通信、最终一致性、分布式锁以及服务治理(限流、熔断)等微服务特有的模式和工具,来解决或规避分布式环境下的“死锁”及其变种问题。这要求我们对整个系统架构有更全面的理解,而不仅仅是单个服务的代码逻辑。
以上就是Java并发编程避坑指南:8种常见死锁场景与解决方案的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号