基于CAS无锁实现线程安全转账:为何需用显式锁?
内存账户线程安全转账:CAS方案的疑问与分析
我的问题
我在实现内存账户间的线程安全转账功能时,用CAS实现了一个保证最终余额一致性的方案。但网上同类方案几乎都用显式锁来保证事务原子性——我的实现里,转账的取款、存款操作虽非原子执行,但最终状态一致,也不会出现负余额。我想搞清楚几个问题:
- 为什么这类场景普遍要用到显式锁?
- 我的实现会在什么情况下丢失一致性?
- 我认为这个实现已经是性能最优的方案,这个判断是否准确?
我的实现代码
Account类(账户核心逻辑)
public class Account { private final long id; private final AtomicReference<BigDecimal> balance; public Account(long id, BigDecimal balance) { this.id = id; this.balance = new AtomicReference<>(balance); } public long getId() { return id; } public void withdraw(BigDecimal amount) { BigDecimal currentBalance, newBalance; do { currentBalance = balance.get(); if(currentBalance.compareTo(amount) < 0) { throw new InsufficientBalanceException("Unable to withdraw amount: " + amount + " which is greater than existing balance: " + balance); } newBalance = currentBalance.subtract(amount); } while (!balance.compareAndSet(currentBalance, newBalance)); } public void deposit(BigDecimal amount) { BigDecimal currentBalance, newBalance; do { currentBalance = balance.get(); newBalance = currentBalance.add(amount); } while (!balance.compareAndSet(currentBalance, newBalance)); } private static class InsufficientBalanceException extends RuntimeException { public InsufficientBalanceException(String msg) { super(msg); } } }
AccountServiceAllowVariance类(转账服务)
public class AccountServiceAllowVariance { public boolean transfer(Account source, Account destination, BigDecimal amount) { Objects.requireNonNull(source, "``from` account cannot be null"); Objects.requireNonNull(destination, "`to` account cannot be null"); Objects.requireNonNull(amount, "`amount` cannot be null"); if(source.equals(destination)) { throw new IllegalArgumentException("`from` & `to` accounts cannot be same"); } if(amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("`amount` must be greater than 0"); } source.withdraw(amount); destination.deposit(amount); return true; } }
问题解答
1. 为什么多数方案选择显式锁?
显式锁(比如ReentrantLock)核心是保证转账操作的原子性——取款和存款两个动作要么全部成功,要么全部失败,不会出现中间状态。而你的方案做不到这一点:
- 一旦取款成功后出现异常(比如线程被强制终止、JVM崩溃,甚至存款方法抛出未捕获的异常),存款动作就不会执行,钱会“凭空消失”,系统总余额直接减少,一致性彻底丢失。
- 显式锁能提供即时一致性,而非最终一致性。很多业务(尤其是金融场景)要求任何时刻查询总余额都是准确的,不能接受“转账过程中总余额暂时不对,过会儿才恢复”的情况。
2. 你的实现会在哪些场景丢失一致性?
- 中间环节异常中断:这是最直接的场景——源账户扣款成功后,目标账户还没完成存款,程序就意外终止,此时总余额少了这笔转账的金额,且永远无法自动恢复。
- 瞬时一致性破坏:虽然最终余额会一致,但在转账的“扣款完成、存款未完成”窗口内,任何读取总余额的操作都会得到错误结果。如果业务要求实时一致性(比如实时对账),这就是严重问题。
- 重试逻辑导致的不一致:如果转账服务被加入重试机制,第一次转账的扣款成功但存款失败时,重试会触发第二次扣款,此时源账户余额不足会抛出异常,但第一次扣的钱已经无法追回,最终源账户少钱、目标账户没加钱,一致性丢失。
3. 关于“性能最优”的判断
你的方案在低冲突、无异常的场景下性能确实出色——CAS是乐观锁,没有锁阻塞的开销,自旋次数少的时候效率很高。但这个“最优”是有前提的:
- 在高冲突场景(比如同一个账户被高频转账)下,CAS的自旋会持续消耗CPU资源,此时显式锁的性能反而更好——因为线程阻塞后会释放CPU,不会空转。
- 性能最优必须结合业务需求:如果业务要求强一致性,你的方案根本不满足,谈“最优”就没有意义。只有在业务可以接受最终一致性、且账户冲突率低的场景下,你的方案才是性能较好的选择。
内容的提问来源于stack exchange,提问作者Murtaza Hasan
相关产品推荐
相关产品推荐

