首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 05 | 一不小心就死锁了,怎么办? 在Java并发编程的广阔领域中,死锁是一个让开发者头疼不已的问题。它如同一道无形的枷锁,悄无声息地限制了程序的运行,导致程序挂起甚至崩溃。本章将深入探讨死锁的本质、成因、识别方法以及一系列预防与解决策略,帮助读者在Java并发编程的实战中避开这一陷阱。 #### 一、死锁的本质与危害 **死锁的本质**:死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。在Java中,这通常发生在多个线程尝试以不同的顺序获取多个锁时。 **死锁的危害**: 1. **程序挂起**:死锁导致涉及的线程无限期地等待对方释放锁,从而使得整个程序或部分功能无法继续执行。 2. **资源浪费**:死锁发生时,涉及的资源(如CPU时间、内存等)被无意义地占用,无法被有效利用。 3. **程序复杂度增加**:为了检测和解决死锁,需要增加额外的逻辑,使得程序的复杂度和维护成本上升。 #### 二、死锁的成因分析 死锁的形成通常需要满足以下四个必要条件,这四个条件也被称为“死锁的四个必要条件”: 1. **互斥条件**:资源是互斥的,即任何时刻只有一个线程能使用。 2. **请求与保持条件**:一个线程至少持有一个资源,并等待获取另一个当前被其他线程持有的资源。 3. **不可剥夺条件**:资源只能由持有它的线程主动释放,不能被其他线程强制剥夺。 4. **循环等待条件**:系统中存在至少一个循环,其中每个线程都在等待下一个线程持有的资源。 在Java并发编程中,常见的导致死锁的场景包括: - **多个线程以不同的顺序尝试获取多个锁**。 - **锁粒度设计不合理**,如过细或过粗的锁划分。 - **错误地使用同步工具**,如`wait()`、`notify()`/`notifyAll()`、`ReentrantLock`等,没有遵循正确的使用模式。 #### 三、识别死锁的方法 1. **日志分析**:通过查看应用程序的日志,特别是线程锁获取和释放的日志,可以初步判断是否存在死锁风险或已经发生死锁。 2. **JConsole/VisualVM等监控工具**:这些Java性能监控工具可以帮助开发者查看线程状态,包括哪些线程处于等待状态,以及它们正在等待什么锁。 3. **线程转储(Thread Dump)**:通过`jstack`等工具获取Java进程的线程转储信息,可以详细看到每个线程的状态、持有的锁以及等待的锁,是诊断死锁问题的强有力工具。 4. **静态代码分析**:使用IDE或专门的并发分析工具,通过静态分析代码中的锁使用模式,预测可能的死锁情况。 #### 四、预防死锁的策略 1. **避免嵌套锁**:尽量减少在同一方法或代码块中多次获取锁的情况,特别是避免嵌套锁。如果必须嵌套,确保锁的获取顺序在所有地方都是一致的。 2. **使用锁超时机制**:在尝试获取锁时设置超时时间,如果超时未获取到锁,则释放已持有的锁并尝试其他策略,避免永久等待。 3. **锁顺序一致性**:在应用中全局定义锁的获取顺序,所有线程都遵循这一顺序来避免循环等待。 4. **减少锁的粒度**:通过细化锁的范围,减少线程间对共享资源的竞争,从而降低死锁的风险。 5. **使用可重入锁(ReentrantLock)的公平锁**:虽然公平锁可能会降低性能,但它能保证锁的获取顺序,从而减少死锁的可能性。 6. **避免在锁保护区内调用外部资源或不确定的代码**:这样做可以避免因外部因素(如网络延迟、数据库查询等)导致的长时间持有锁,进而引发死锁。 #### 五、解决死锁的策略 一旦确认程序中存在死锁,就需要采取措施来解决。解决死锁的策略通常包括: 1. **中断死锁线程**:如果可能,通过中断或停止死锁的线程来打破死锁状态。但这种方法需要谨慎使用,因为它可能导致数据不一致或程序逻辑错误。 2. **回滚事务**:如果死锁发生在事务处理过程中,可以考虑回滚部分或全部事务,然后重新执行。 3. **重新设计同步策略**:深入分析死锁的原因,重新设计锁的使用策略,如调整锁的顺序、改变锁的粒度或引入新的同步机制。 4. **使用死锁检测工具**:在开发阶段或生产环境中引入死锁检测工具,实时监控并预警潜在的死锁情况,以便及时采取措施。 5. **资源隔离**:通过资源隔离的方式,将可能引发死锁的资源或操作分离到不同的系统或进程中,从而减少死锁的风险。 #### 六、实战案例分析 假设我们有一个简单的银行转账系统,其中两个账户需要同时进行转账操作,且每个操作都需要锁定两个账户以确保转账的一致性。如果两个转账操作以相反的顺序锁定账户,就可能导致死锁。 **解决方案**: - **统一锁顺序**:确保所有转账操作都按照相同的顺序(如从账户A到账户B)来锁定账户。 - **使用显式锁(ReentrantLock)**:通过显式锁可以更容易地控制锁的获取和释放,以及设置锁的超时时间。 - **引入超时机制**:在尝试获取锁时设置超时时间,一旦超时则释放已持有的锁并重新尝试或回滚操作。 #### 七、总结 死锁是Java并发编程中必须面对和解决的问题。通过深入理解死锁的本质、成因、识别方法及预防与解决策略,我们可以在实际开发中有效地避免死锁的发生,提高程序的稳定性和可靠性。记住,预防总是优于治疗,合理的同步设计和锁策略是避免死锁的关键。同时,当死锁发生时,冷静分析、迅速定位并采取合适的解决策略,也是每一个并发编程者必备的能力。
上一篇:
04 | 互斥锁(下):如何用一把锁保护多个资源?
下一篇:
06 | 用“等待-通知”机制优化循环等待
该分类下的相关小册推荐:
Java语言基础2-运算符
Mybatis合辑5-注解、扩展、SQL构建
Java高并发秒杀入门与实战
Java必知必会-Maven初级
Java语言基础5-面向对象初级
经典设计模式Java版
Mybatis合辑1-Mybatis基础入门
Java语言基础16-JDK8 新特性
Java语言基础6-面向对象高级
JAVA 函数式编程入门与实践
Java语言基础10-Java中的集合
手把手带你学习SpringBoot-零基础到实战