You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 20:45:12