跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

在Java里如何避免死锁问题_Java并发设计实践说明

死锁最常见原因是线程以不同顺序获取同一组锁,解决方法是统一加锁顺序、使用tryLock超时机制、减小锁粒度、避免嵌套锁与外部可重入调用,并确保锁语义一致性。 按固定顺序获取锁 死锁最常见原因是多个线程以不同顺序请求同一组锁。比如线程 A 先锁
resourceA
再锁
resourceB
,而线程 B 反过来先锁
resourceB
再锁
resourceA
,就可能形成环路等待。 解决办法是强制所有线程按全局一致的顺序加锁。通常用对象哈希值排序:
public void transfer(Account from, Account to, BigDecimal amount) { // 确保总是先锁 hash 值小的对象 Object firstLock = from.hashCode() < to.hashCode() ? from : to; Object secondLock = from.hashCode() < to.hashCode() ? to : from; synchronized (firstLock) { synchronized (secondLock) { // 执行转账逻辑 } } }
注意:
hashCode()
在 JVM 生命周期内不保证稳定,生产环境建议用唯一且不可变的字段(如
accountId
)做比较 避免在同步块内调用外部方法或阻塞操作,否则会延长持锁时间,增加竞争概率 如果锁对象可能为
null
,必须提前校验,否则
NullPointerException
会绕过锁保护逻辑 使用 tryLock() 设置超时
ReentrantLock.tryLock(long, TimeUnit)
能让线程在拿不到锁时主动放弃,而不是无限等待,从根本上打破死锁必要条件之一——“循环等待”。 典型用法: 立即学习 “ Java免费学习笔记(深入) ”; Eclipse导入Android或其他的JAVA项目的正确方法 WORD版 本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看 下载
ReentrantLock lockA = new ReentrantLock(); ReentrantLock lockB = new ReentrantLock(); boolean success = false; try { if (lockA.tryLock(1, TimeUnit.SECONDS)) { try { if (lockB.tryLock(1, TimeUnit.SECONDS)) { try { // 临界区操作 success = true; } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
超时时间不宜设为 0(即非阻塞),否则重试逻辑容易退化成忙等,浪费 CPU 必须严格遵循“获取成功才继续下一步,失败则释放已获锁”的模式,否则可能造成锁泄漏 嵌套多层
tryLock()
时,解锁顺序不必与加锁顺序严格相反,但每把锁都必须有对应
unlock()
且只调用一次 减少锁粒度和同步范围 很多死锁其实源于过度同步:把本可并行的操作强行串行化,人为扩大了锁持有窗口和依赖链。 例如,对整个
ConcurrentHashMap
加锁来执行单个
put()
是错的;又比如在
synchronized
方法里调用远程 HTTP 接口。 优先用
java.util.concurrent
包中的线程安全类(如
ConcurrentHashMap
、
AtomicInteger
),它们内部已做细粒度控制 同步块只包裹真正需要互斥的代码段,像日志记录、参数校验、异步提交这类操作应移出同步区 避免在持有锁期间调用第三方库方法,除非明确其不涉及加锁或 I/O —— 很多框架内部会隐式加锁(如某些 ORM 的 session 同步机制 ) 避免嵌套锁 + 外部可重入调用 当一个已持锁的方法被外部间接调用(比如通过回调、事件监听、Spring AOP 代理),而该外部调用又试图获取同一把锁,就可能触发重入死锁——尤其在未使用
ReentrantLock
或
synchronized
这类可重入锁时。 更危险的是“伪重入”:两个不同锁对象,但逻辑上代表同一资源(如
UserLock(userId)
和
OrderLock(orderId)
实际共享账户余额状态)。 检查所有回调接口是否运行在相同线程上下文;若不确定,改用异步解耦(如
CompletableFuture
或消息队列) 不要基于运行时参数动态生成锁对象(如
new Object()
),这会让锁无法被其他线程识别和竞争 Spring 中使用
@Transactional
或
@Async
时,注意代理对象可能改变调用栈,导致锁失效或重复加锁 实际中最容易被忽略的,是锁的语义一致性:同一业务概念(如“用户资金”)在不同模块中用了不同锁对象或不同加锁策略,表面看没死锁,但会在高并发下暴露竞态漏洞。

相关文章