Java并发:同步方法死锁预防技巧
一分耕耘,一分收获!既然打开了这篇文章《Java并发:同步方法死锁预防技巧》,就坚持看下去吧!文中内容包含等等知识点...希望你能在阅读本文后,能真真实实学到知识或者帮你解决心中的疑惑,也欢迎大佬或者新人朋友们多留言评论,多给建议!谢谢!

理解同步方法中的死锁风险
在Java并发编程中,synchronized关键字是实现线程同步的常用机制,它确保同一时刻只有一个线程可以执行特定的代码块或方法。然而,不当的同步策略可能导致死锁,即两个或多个线程无限期地等待彼此释放资源。
一个经典的死锁场景发生在多个线程尝试获取多个锁,但获取顺序不一致时。考虑一个transferMoney方法,它需要同步两个Account对象以执行转账操作:
public class Account {
private UUID id;
private float balance;
// 构造函数、getter/setter等
public UUID getId() {
return id;
}
public void debit(float amount) {
this.balance -= amount;
}
public void credit(float amount) {
this.balance += amount;
}
}
public class TransferService {
public void transferMoney(Account a, Account b, float value) {
synchronized(a) { // 线程1获取了A的锁
synchronized(b) { // 线程1尝试获取B的锁
// 执行转账逻辑
a.debit(value);
b.credit(value);
}
}
}
}假设现在有两个线程同时调用transferMoney方法:
- 线程1调用transferMoney(accountA, accountB, 100)
- 线程2调用transferMoney(accountB, accountA, 50)
如果线程1成功获取了accountA的锁,并紧接着线程2成功获取了accountB的锁,那么:
- 线程1会等待accountB的锁(已被线程2持有)
- 线程2会等待accountA的锁(已被线程1持有)
这将导致典型的死锁,两个线程都无法继续执行。
策略一:强制一致的锁获取顺序
避免死锁的关键在于确保所有线程以相同的、预定义的顺序获取多个锁。这意味着我们不能依赖于方法参数的传入顺序,而应该基于锁对象的某个固有属性来确定其获取顺序。
为了实现这一点,我们可以为每个Account对象引入一个唯一标识符(例如UUID或Long ID),并约定在获取锁时,总是先获取ID较小的账户的锁,再获取ID较大的账户的锁。
首先,修改Account类,使其包含一个用于比较的唯一ID:
import java.util.Comparator;
import java.util.UUID;
import java.util.function.BinaryOperator;
public class Account {
private UUID id;
private float balance;
public Account(UUID id, float initialBalance) {
this.id = id;
this.balance = initialBalance;
}
public UUID getId() {
return id;
}
public void debit(float amount) {
if (this.balance < amount) {
throw new IllegalArgumentException("Insufficient funds.");
}
this.balance -= amount;
}
public void credit(float amount) {
this.balance += amount;
}
public float getBalance() {
return balance;
}
@Override
public String toString() {
return "Account{" + "id=" + id.toString().substring(0, 8) + ", balance=" + balance + '}';
}
// 辅助方法,用于确定两个账户中ID较小的那个
public static final BinaryOperator FIRST =
BinaryOperator.minBy(Comparator.comparing(Account::getId));
// 辅助方法,用于确定两个账户中ID较大的那个
public static final BinaryOperator SECOND =
BinaryOperator.maxBy(Comparator.comparing(Account::getId));
} 接下来,修改transferMoney方法,使用FIRST和SECOND辅助方法来确定锁的获取顺序:
public class TransferService {
public void transferMoney(Account a, Account b, float value) {
// 确保不能向同一个账户转账
if (a.getId().equals(b.getId())) {
throw new IllegalArgumentException("Cannot transfer money to the same account.");
}
// 确定锁的获取顺序:总是先获取ID较小的账户的锁,再获取ID较大的账户的锁
Account firstLock = Account.FIRST.apply(a, b);
Account secondLock = Account.SECOND.apply(a, b);
synchronized (firstLock) {
synchronized (secondLock) {
// 执行转账逻辑
System.out.println(Thread.currentThread().getName() + " acquired locks for " + firstLock.getId().toString().substring(0, 8) + " and " + secondLock.getId().toString().substring(0, 8));
try {
// 模拟转账耗时
Thread.sleep(100);
firstLock.debit(value);
secondLock.credit(value);
System.out.println(Thread.currentThread().getName() + " transferred " + value + " from " + a.getId().toString().substring(0, 8) + " to " + b.getId().toString().substring(0, 8));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}通过这种方式,无论transferMoney方法被调用时accountA和accountB的传入顺序如何,firstLock和secondLock变量总是会引用到具有一致ID顺序的账户。例如,如果accountA.id小于accountB.id,那么firstLock总是accountA,secondLock总是accountB。这样,所有线程都会以synchronized(account_with_smaller_id) { synchronized(account_with_larger_id) {...} }的顺序获取锁,从而有效避免死锁。
策略二:使用java.util.concurrent.locks.Lock
除了synchronized关键字,Java并发API还提供了java.util.concurrent.locks.Lock接口,它提供了更灵活的锁机制。Lock接口的实现(如ReentrantLock)允许更精细地控制锁的获取和释放,尤其是在处理死锁时提供了额外的工具。
Lock接口的核心思想是,当一个线程需要获取多个锁时,如果它无法一次性获取所有必需的锁,就应该释放已经持有的锁,并稍后重试。这可以通过tryLock()方法实现,它尝试获取锁而不阻塞,并返回一个布尔值指示是否成功获取。
使用Lock的基本模式如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 假设每个Account对象内部有一个ReentrantLock
public class AccountWithLock {
private UUID id;
private float balance;
private final Lock lock = new ReentrantLock(); // 每个账户一个独立的锁
// ... 构造函数、getter/setter等
public Lock getLock() {
return lock;
}
}
public class TransferServiceWithLock {
public void transferMoney(AccountWithLock a, AccountWithLock b, float value) {
// 同样,先确定一致的锁获取顺序
AccountWithLock first = AccountWithLock.FIRST.apply(a, b); // 假设AccountWithLock也有FIRST/SECOND
AccountWithLock second = AccountWithLock.SECOND.apply(a, b);
Lock lock1 = first.getLock();
Lock lock2 = second.getLock();
boolean acquired1 = false;
boolean acquired2 = false;
try {
// 尝试获取第一个锁
acquired1 = lock1.tryLock();
if (acquired1) {
// 尝试获取第二个锁
acquired2 = lock2.tryLock();
if (acquired2) {
// 成功获取所有锁,执行转账
a.debit(value);
b.credit(value);
} else {
// 未能获取第二个锁,释放第一个锁,稍后重试
lock1.unlock();
}
}
} finally {
// 确保所有获取的锁都被释放
if (acquired2) {
lock2.unlock();
}
if (acquired1) { // 再次检查,因为如果acquired2失败,acquired1可能仍为true
lock1.unlock();
}
}
}
}注意事项:
- tryLock()方法可以带超时参数,避免无限等待。
- finally块对于确保锁的释放至关重要,即使在转账过程中发生异常。
- 使用Lock接口时,同样需要遵循一致的锁获取顺序原则,以简化死锁处理逻辑。
总结与最佳实践
死锁是并发编程中的一个常见陷阱,但通过遵循一些基本原则可以有效避免。
- 统一锁获取顺序: 这是预防多资源死锁最核心的策略。通过对需要同步的资源进行排序(例如,基于对象的唯一ID),并强制所有线程按照该顺序获取锁,可以消除循环等待的条件。
- 避免嵌套锁: 尽量减少在持有锁的情况下再去获取另一个锁的情况。如果必须嵌套,务必确保锁的获取顺序是严格一致的。
- 使用java.util.concurrent.locks.Lock: 对于更复杂的并发场景,ReentrantLock等Lock实现提供了比synchronized更强大的功能,如可中断的锁获取(lockInterruptibly())、非阻塞的锁获取(tryLock())以及公平性选项。这些特性可以帮助开发者构建更健壮的死锁恢复机制。
- 设置锁超时: 在使用Lock时,tryLock(long timeout, TimeUnit unit)方法允许线程在指定时间内尝试获取锁。如果超时仍未获取,线程可以选择放弃并回退,而不是无限期等待。
通过深入理解死锁的成因并采纳上述策略,开发者可以显著提高并发应用程序的稳定性和可靠性。
今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~
SeleniumChrome代理设置全攻略
- 上一篇
- SeleniumChrome代理设置全攻略
- 下一篇
- PHP中const与define的区别解析
-
- 文章 · java教程 | 6小时前 | Java · 异步编程 · 后端开发 · CompletableFuture · 接口聚合 · java 结果合并 completablefuture 并行调用 超时兜底
- Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并
- 428浏览 收藏
-
- 文章 · java教程 | 8小时前 | Java · 线程安全 · DateTimeFormatter · 日期处理 · 并发问题 · java 线程安全 日期格式化 threadlocal SimpleDateFormat DateTimeFormatter
- Java SimpleDateFormat 日期偶发错乱怎么办:从共享实例到线程安全一步步排查
- 481浏览 收藏
-
- 文章 · java教程 | 2天前 | http接口 · httpclient · Java教程 · 接口调试 · 超时处理 · java 接口调用 httpclient 超时控制 状态码 响应体
- Java HttpClient 调接口实战:超时、状态码和响应体这样处理
- 224浏览 收藏
-
- 文章 · java教程 | 2天前 | 时间处理 · instant · Java教程 · 时区转换 · DateTimeFormatter · java DateTimeFormatter java.time 时区处理 ZoneId INSTANT
- Java 时间与时区处理实战:Instant、ZoneId 和 DateTimeFormatter 怎么配
- 461浏览 收藏
-
- 文章 · java教程 | 2天前 | Java · Stream · 集合统计 · 分组聚合 · Collectors · java Stream Collectors groupingBy counting summarizingInt
- Java Stream 分组统计实战:groupingBy、counting 和 summarizingInt 怎么用
- 478浏览 收藏
-
- 文章 · java教程 | 2天前 | Java · 文件读取 · 异常处理 · 资源管理 · try-with-resources · java 异常处理 try-with-resources 资源关闭 AutoCloseable 文件流
- Java try-with-resources 资源关闭实战:文件流和目录扫描这样写更稳
- 268浏览 收藏
-
- 文章 · java教程 | 2天前 | Java教程 · 后端开发 · BigDecimal · 金额计算 · java 舍入 bigdecimal 浮点误差 金额计算 RoundingMode
- Java BigDecimal 金额计算实战:避免浮点误差和舍入问题
- 324浏览 收藏
-
- 文章 · java教程 | 3天前 | 异步编程 · Java教程 · 超时治理 · CompletableFuture · java 异步任务 超时处理 completablefuture orTimeout completeOnTimeout
- Java CompletableFuture 超时处理实战:orTimeout 和兜底结果怎么选
- 421浏览 收藏
-
- 文章 · java教程 | 1星期前 | 并发编程 · 生产实践 · Java教程 · JDK25 · 虚拟线程 · 虚拟线程 Java 25 JEP 505 Structured Concurrency StructuredTaskScope
- Java 25 Structured Concurrency 实战:别让 CompletableFuture 把超时拖散
- 443浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 14次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 23次使用
-
- Red Skill
- 小红书创作服务平台为小红书创作者和机构提供视频上传、数据分析、粉丝管理、创作指导等多项运营服务,助力用户解锁更多创作者专属功能,体验高效创作!
- 31次使用
-
- MiMo Code
- MiMo Code 是小米大模型团队开源的新一代 AI 编程助手,面向开发者提供代码理解、生成与辅助开发能力,适合作为 AI 编程工具收藏和体验。
- 121次使用
-
- TRAE Work
- TRAE AI IDE | 国内首款 AI 原生集成开发环境,深度集成 Doubao-1.5-pro 与 DeepSeek 模型,支持中文自然语言一键生成完整代码框架,实时预览前端效果并智能修复 BUG。首创 Builder 模式实现需求到代码的自动化开发,兼容 Windows/macOS 系统,官网下载即用。
- 148次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

