用synchronized实现银行转账安全需按账户id升序加锁以避免死锁,即先锁id小的账户、再锁id大的账户,并在双重同步块内完成扣款与入账操作,确保原子性。

用 synchronized 实现银行账户转账安全,核心是避免多个线程同时修改两个账户余额导致数据不一致(比如扣款成功但入账失败)。单纯给转账方法加 synchronized 不够——必须保证对两个账户的操作是**原子的、顺序一致的**,否则可能引发死锁或竞态条件。
问题关键:锁的粒度和顺序
两个账户 A 和 B 转账时,若线程1按“A→B”顺序加锁,线程2按“B→A”顺序加锁,就容易死锁。解决办法是:**始终按固定顺序获取锁**,比如按账户 ID 数值大小排序,先锁 ID 小的,再锁 ID 大的。
正确做法:同步代码块 + 有序加锁
不要用 synchronized 方法(锁的是 this,无法控制多对象锁序),而是用同步代码块显式锁定两个账户对象,并确保加锁顺序一致:
- 比较两个账户对象的 hash 值或自定义唯一 ID(如 accountNo)
- 先对较小 ID 的账户加锁,再对较大 ID 的账户加锁
- 在双重锁内完成扣款和入账,确保原子性
示例代码片段:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
public void transfer(Account from, Account to, BigDecimal amount) {
// 确保加锁顺序:小ID先锁,大ID后锁
Object firstLock = from.getAccountNo().compareTo(to.getAccountNo())
账户类需保证锁对象稳定
Account 对象本身要作为锁对象,所以不能每次 new 新对象,也不能让 accountNo 可变。推荐:
- Account 是不可变 ID + 可变余额的结构
- 锁的是 Account 实例(不是 accountNo 字符串),且该实例在整个系统中唯一(如从 Map 缓存中获取)
- 避免用 String 或 Integer 自动装箱对象作锁(有常量池/缓存干扰)
补充建议:配合其他手段更稳妥
synchronized 能解决基础并发问题,但在生产环境建议叠加:
- 转账前校验余额(已包含在示例中)
- 使用数据库事务兜底(即使 JVM 层出错,DB 层仍能回滚)
- 考虑用
ReentrantLock配合 tryLock() 实现超时机制,避免无限等待 - 高并发场景下,可引入分段锁或乐观锁(CAS)减少争用
不复杂但容易忽略:锁顺序一致性和锁对象稳定性,才是用 synchronized 做转账安全的真正要点。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










