什么是CAS?CAS的底层原理是什么?哪些场景会用到?

面试中回答 CAS(Compare-And-Swap) 的底层原理和应用场景时,可以采用 “概念 + 原理 + 代码 + 场景” 的结构化方式,既清晰又有深度。以下是一个参考回答:

🗣️ 标准回答模板

概念
CAS 是一种无锁算法,用于实现多线程环境下的原子操作。它的核心思想是:先比较变量的当前值是否等于预期值,如果相等则更新为新值,否则操作失败。整个过程由 CPU 硬件保证原子性。

底层原理

  1. 硬件支持:CAS 依赖 CPU 的原子指令(如 x86 的 cmpxchg 指令),这些指令在硬件层面保证了比较和交换操作的不可分割性。
  2. Java 实现
    • 在 Java 中,sun.misc.Unsafe 类提供了 compareAndSwapIntcompareAndSwapObject 等本地方法,直接调用 CPU 的原子指令。
    • AtomicIntegerAtomicLong 等原子类通过 Unsafe 实现 CAS 操作。例如:

      java

      public final boolean compareAndSet(int expectedValue, int newValue) {
          return U.compareAndSwapInt(this, VALUE, expectedValue, newValue);
      }
      

      其中 U 是 Unsafe 实例,VALUE 是变量的内存偏移量。

应用场景

  1. 原子类操作AtomicIntegerAtomicBoolean 等,用于实现无锁计数器、状态标记。
    • 示例:

      java

      private AtomicInteger count = new AtomicInteger(0);
      public void increment() {
          count.incrementAndGet(); // 底层用 CAS 实现
      }
      
  2. 并发容器ConcurrentHashMapConcurrentLinkedQueue 等,用 CAS 替代传统锁提升性能。
  3. 乐观锁:数据库更新时的冲突检测(如 version 字段),或分布式锁中的 SETNX 命令。
  4. 状态转换:确保资源只被初始化一次(如单例模式):

    java

    private static final AtomicReference<Resource> INSTANCE = new AtomicReference<>();
    public static Resource getInstance() {
        if (INSTANCE.get() == null) {
            INSTANCE.compareAndSet(null, new Resource());
        }
        return INSTANCE.get();
    }
    

💡 加分回答(深入理解)

  • 与锁的对比

    • 锁是悲观的,认为总会发生冲突,因此先加锁;
    • CAS 是乐观的,认为冲突很少,先尝试操作,失败后重试。
      因此,CAS 在低冲突场景下性能远高于锁,但高冲突时可能因频繁重试导致 CPU 耗尽。
  • ABA 问题

    • 如果变量从 A→B→A,CAS 会误认为值未变。
    • 解决方案:使用 AtomicStampedReference 或 AtomicMarkableReference 附加版本号或标记位。
    • 用一个银行转账的案例演示如何用 AtomicStampedReference 解决 CAS 的 ABA 问题
    • 🌰 案例:银行账户转账
      假设账户余额从 100 元转出 50 元,再转入 50 元,余额变回 100 元。如果直接用 CAS 操作,可能会忽略中间的转账操作。
      import java.util.concurrent.atomic.AtomicInteger;
      
      public class ABADemo {
          private static AtomicInteger balance = new AtomicInteger(100);
      
          public static void main(String[] args) throws InterruptedException {
              // 线程1:模拟正常转账
              Thread t1 = new Thread(() -> {
                  int expected = balance.get();
                  System.out.println("T1 准备转出50,预期余额: " + expected);
                  
                  // 模拟处理延迟
                  try { Thread.sleep(1000); } catch (InterruptedException e) {}
                  
                  if (balance.compareAndSet(expected, expected - 50)) {
                      System.out.println("T1 转出成功,当前余额: " + balance.get());
                  } else {
                      System.out.println("T1 转出失败,余额已被修改");
                  }
              });
      
              // 线程2:模拟异常操作(先转出再转入)
              Thread t2 = new Thread(() -> {
                  try { Thread.sleep(200); } catch (InterruptedException e) {}
                  
                  // 转出50
                  int expected1 = balance.get();
                  if (balance.compareAndSet(expected1, expected1 - 50)) {
                      System.out.println("T2 转出50,当前余额: " + balance.get());
                  }
                  
                  // 转入50
                  int expected2 = balance.get();
                  if (balance.compareAndSet(expected2, expected2 + 50)) {
                      System.out.println("T2 转入50,当前余额: " + balance.get());
                  }
              });
      
              t1.start();
              t2.start();
              t1.join();
              t2.join();
          }
      }

      可能的输出结果

    • T1 准备转出50,预期余额: 100
      T2 转出50,当前余额: 50
      T2 转入50,当前余额: 100
      T1 转出成功,当前余额: 50  // ❌ 出现 ABA 问题:余额被错误地扣减了两次

      ✅ 使用 AtomicStampedReference 解决 ABA 问题

    • import java.util.concurrent.atomic.AtomicStampedReference;
      
      public class ABASolution {
          // 初始余额100,版本号0
          private static AtomicStampedReference<Integer> balance = 
              new AtomicStampedReference<>(100, 0);
      
          public static void main(String[] args) throws InterruptedException {
              // 线程1:模拟正常转账
              Thread t1 = new Thread(() -> {
                  int[] stampHolder = new int[1];
                  int expected = balance.get(stampHolder);
                  int expectedStamp = stampHolder[0];
                  System.out.println("T1 准备转出50,预期余额: " + expected);
                  
                  // 模拟处理延迟
                  try { Thread.sleep(1000); } catch (InterruptedException e) {}
                  
                  // 同时检查值和版本号
                  if (balance.compareAndSet(expected, expected - 50, expectedStamp, expectedStamp + 1)) {
                      System.out.println("T1 转出成功,当前余额: " + balance.getReference());
                  } else {
                      System.out.println("T1 转出失败,余额已被修改");
                  }
              });
      
              // 线程2:模拟异常操作(先转出再转入)
              Thread t2 = new Thread(() -> {
                  try { Thread.sleep(200); } catch (InterruptedException e) {}
                  
                  // 转出50(版本号+1)
                  int[] stampHolder1 = new int[1];
                  int expected1 = balance.get(stampHolder1);
                  int stamp1 = stampHolder1[0];
                  if (balance.compareAndSet(expected1, expected1 - 50, stamp1, stamp1 + 1)) {
                      System.out.println("T2 转出50,当前余额: " + balance.getReference());
                  }
                  
                  // 转入50(版本号再+1)
                  int[] stampHolder2 = new int[1];
                  int expected2 = balance.get(stampHolder2);
                  int stamp2 = stampHolder2[0];
                  if (balance.compareAndSet(expected2, expected2 + 50, stamp2, stamp2 + 1)) {
                      System.out.println("T2 转入50,当前余额: " + balance.getReference());
                  }
              });
      
              t1.start();
              t2.start();
              t1.join();
              t2.join();
          }
      }

      输出结果

    • T1 准备转出50,预期余额: 100
      T2 转出50,当前余额: 50
      T2 转入50,当前余额: 100
      T1 转出失败,余额已被修改  // ✅ 正确处理了 ABA 问题

      AtomicStampedReference 维护一个 值 + 版本号 的组合:

    • 每次修改时,版本号递增(即使值变回原来的值,版本号也不同)
    • CAS 操作需要同时匹配值和版本号,只有两者都符合预期才会成功
    • 关键方法:
      // 获取当前值和版本号(通过数组传递)
      int[] stampHolder = new int[1];
      int value = reference.get(stampHolder);
      int stamp = stampHolder[0];
      
      // 原子性地比较并设置(需要同时匹配值和版本号)
      reference.compareAndSet(
          expectedValue,  // 预期值
          newValue,       // 新值
          expectedStamp,  // 预期版本号
          newStamp        // 新版本号
      );

  • 实际项目经验

    • 例如在分布式缓存系统中,用 AtomicInteger 统计缓存命中率;
    • 在状态机设计中,用 AtomicBoolean 控制服务启停;
    • 在数据库操作中,用 CAS 实现乐观锁防止更新冲突。

🚫 避坑指南

  • 不要混淆概念:CAS 是一种无锁算法,而 synchronized 是基于锁的实现。
  • 不要忽略 ABA 问题:必须提到 ABA 问题及其解决方案。
  • 结合场景回答:如果面试官追问 “CAS 和 synchronized 怎么选?”,可以回答:
    “CAS 适用于读多写少、冲突少的场景(如计数器),而 synchronized 适用于写多读少、冲突频繁的场景。”
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值