JUC并发编程 CAS运行机制详解

120 阅读10分钟

CAS运行机制详解

1. CAS的出现背景

1.1 没有CAS之前的问题

在多线程环境下,为了保证线程安全的i++操作(基本数据类型),我们必须使用synchronized等重量级锁机制:

public class CounterWithLock {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // 需要加锁保证原子性
    }
    
    public synchronized int getCount() {
        return count;
    }
}

传统加锁方式的问题:

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant T3 as 线程3
    participant Lock as 锁对象
    participant Memory as 共享内存
    
    T1->>Lock: 请求获取锁
    Lock->>T1: 获取成功
    T2->>Lock: 请求获取锁
    Lock->>T2: 阻塞等待
    T3->>Lock: 请求获取锁
    Lock->>T3: 阻塞等待
    
    T1->>Memory: 执行i++操作
    T1->>Lock: 释放锁
    Lock->>T2: 唤醒线程2
    T2->>Memory: 执行i++操作
    T2->>Lock: 释放锁
    Lock->>T3: 唤醒线程3
    T3->>Memory: 执行i++操作

1.2 有了CAS之后的改进

使用原子类(java.util.concurrent.atomic)可以在多线程环境下保证线程安全的i++操作,类似乐观锁的机制:

public class CounterWithAtomic {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.getAndIncrement();  // 无需加锁,CAS保证原子性
    }
    
    public int getCount() {
        return count.get();
    }
}

CAS机制的优势:

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant T3 as 线程3
    participant Memory as 共享内存
    
    Note over T1,T3: 所有线程可以并发执行
    
    T1->>Memory: CAS(期望值, 新值)
    Memory->>T1: 成功更新
    
    T2->>Memory: CAS(期望值, 新值)
    Memory->>T2: 失败,重试
    T2->>Memory: CAS(新期望值, 新值)
    Memory->>T2: 成功更新
    
    T3->>Memory: CAS(期望值, 新值)
    Memory->>T3: 成功更新

2. CAS是什么

2.1 基本概念

CAS是Compare And Swap的缩写,中文翻译成比较并交换,是实现并发算法时常用到的一种技术。

CAS包含三个操作数:

  • 内存位置(V):要更新的变量的内存地址
  • 预期原值(A):期望的当前值
  • 更新值(B):要设置的新值

2.2 CAS操作原理

执行CAS操作的时候,将内存位置的值与预期原值比较:

  • 如果相匹配:处理器会自动将该位置值更新为新值
  • 如果不匹配:处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
flowchart TD
    A[开始CAS操作] --> B[读取内存值V]
    B --> C{当前值 == 预期值A?}
    C -->|是| D[更新V为新值B] --> E[操作成功\n返回true] --> F[结束]
    C -->|否| G{是否重试?}
    G -->|是| B
    G -->|否| H[操作失败\n返回当前值] --> F

CAS操作的核心逻辑

  1. 读取步骤:从内存位置V读取当前值
  2. 比较步骤:将读取到的当前值与预期值A进行比较
  3. 交换步骤:如果相等,则将内存位置V的值更新为新值B;如果不相等,则不做任何操作
  4. 返回步骤:返回操作是否成功的结果

伪代码表示

// CAS操作的伪代码实现
public boolean compareAndSwap(int* ptr, int expected, int newValue) {
    if (*ptr == expected) {
        *ptr = newValue;
        return true;  // 操作成功
    }
    return false;     // 操作失败
}

2.3 CAS的核心特性

CAS有3个操作数:

  • V(Value):位置内存值
  • A(Assumed):旧的预期值
  • B(New):要修改的更新值

当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来。当它重来重试的这种行为称为——自旋!

2.4 硬件级别的保证

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性:

  • 非阻塞性:不会造成线程阻塞,效率更高
  • 原子性:通过硬件保证,更可靠
  • CPU指令级别:CAS是一条CPU的原子指令(cmpxchg指令)
graph TD
    A[CAS操作] --> B[cmpxchg CPU指令]
    B --> C{多核系统?}
    C -->|是| D[锁定缓存行]
    C -->|否| E[直接执行]
    D --> F[通过缓存一致性协议保证原子性]
    E --> G[执行原子比较交换]
    F --> G
    G --> H[释放缓存行锁定]
    H --> I[返回操作结果]

现代CPU的CAS实现机制:

  1. 缓存行锁定:现代CPU不会锁定整个总线,而是锁定特定的缓存行(Cache Line)
  2. 缓存一致性协议:通过MESI等协议确保多核间的数据一致性
  3. 性能优化:相比锁总线,锁缓存行的粒度更小,性能更好
  4. 原子性保证:在缓存行级别保证CAS操作的原子性

CAS的原子性实际上是CPU通过缓存行锁定实现的,比起用synchronized重量级锁,这里的排他时间要短很多,锁定粒度也更小,所以在多线程情况下性能会比较好。

3. CAS底层原理

3.1 Unsafe类的作用

Unsafe是CAS的核心类,它提供了直接操作内存的能力:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    private volatile int value;
}

Unsafe类的特点:

  1. 直接内存访问:Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据
  2. 本地方法调用:Unsafe类中的所有方法都是native修饰的,直接调用操作系统底层资源
  3. 内存偏移地址:变量valueOffset表示该变量值在内存中的偏移地址
  4. volatile保证可见性:变量value用volatile修饰,保证了多线程之间的内存可见性

3.2 AtomicInteger的getAndIncrement()实现

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);  // 获取当前值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  // CAS操作
    return var5;
}

CAS操作流程详解:

sequenceDiagram
    participant Thread as 调用线程
    participant Unsafe as Unsafe类
    participant Memory as 内存
    participant CPU as CPU指令
    
    Thread->>Unsafe: getAndIncrement()
    Unsafe->>Memory: getIntVolatile(obj, offset)
    Memory->>Unsafe: 返回当前值(var5)
    
    loop CAS自旋
        Unsafe->>CPU: compareAndSwapInt(obj, offset, expected, new)
        CPU->>Memory: 执行cmpxchg指令
        Memory->>CPU: 比较并交换结果
        CPU->>Unsafe: 返回操作结果
        
        alt CAS成功
            Unsafe->>Thread: 返回旧值
        else CAS失败
            Unsafe->>Memory: 重新获取当前值
            Memory->>Unsafe: 返回新的当前值
            Note over Unsafe: 继续自旋重试
        end
    end

3.3 compareAndSwapInt方法分析

// Unsafe类中的native方法
public final native boolean compareAndSwapInt(
    Object var1,    // 对象实例
    long var2,      // 内存偏移量
    int var4,       // 期望值
    int var5        // 新值
);

参数说明:

  • var1:表示要操作的对象
  • var2:表示要操作对象中属性地址的偏移量
  • var4:表示需要修改数据的期望值
  • var5:表示需要修改为的新值

3.4 底层CPU指令实现

graph LR
    A[Java CAS调用] --> B[JVM层面]
    B --> C[Unsafe.compareAndSwapInt]
    C --> D[JNI本地方法]
    D --> E[C++实现]
    E --> F[CPU cmpxchg指令]
    
    subgraph "CPU指令级别"
        F --> G[比较内存值与期望值]
        G --> H{值相等?}
        H -->|是| I[原子性更新内存]
        H -->|否| J[不做任何操作]
        I --> K[返回成功]
        J --> L[返回失败]
    end

4. CAS的缺点与自旋锁

4.1 自旋锁机制

自旋锁(spinlock)是CAS实现的基础。CAS利用CPU指令保证了操作的原子性,以达到锁的效果。自旋是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁

stateDiagram-v2
    [*] --> 尝试获取锁
    尝试获取锁 --> 获取成功: CAS成功
    尝试获取锁 --> 自旋等待: CAS失败
    自旋等待 --> 尝试获取锁: 继续尝试
    获取成功 --> 执行临界区代码
    执行临界区代码 --> 释放锁
    释放锁 --> [*]
    
    note right of 自旋等待
        不断循环判断锁的状态
        消耗CPU但避免线程切换
    end note

自旋锁的优缺点:

  • 优点:减少线程上下文切换的消耗
  • 缺点:循环会消耗CPU资源

4.2 CAS的主要缺点

4.2.1 循环时间长开销很大

当多个线程同时竞争同一个原子变量时,失败的线程会不断自旋重试,导致CPU使用率飙升:

// 高并发场景下的性能问题示例
public class CASPerformanceTest {
    private AtomicInteger counter = new AtomicInteger(0);
    
    public void highConcurrencyIncrement() {
        // 1000个线程同时执行increment
        // 大量线程会在CAS操作上自旋
        counter.getAndIncrement();
    }
}
sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant T3 as 线程3
    participant TN as 线程N
    participant Memory as 共享变量
    
    Note over T1,TN: 高并发场景
    
    T1->>Memory: CAS操作
    T2->>Memory: CAS操作(失败)
    T3->>Memory: CAS操作(失败)
    TN->>Memory: CAS操作(失败)
    
    Memory->>T1: 成功
    Memory->>T2: 失败,自旋重试
    Memory->>T3: 失败,自旋重试
    Memory->>TN: 失败,自旋重试
    
    Note over T2,TN: 大量线程自旋消耗CPU
    
    T2->>Memory: 重试CAS操作
    T3->>Memory: 重试CAS操作
    TN->>Memory: 重试CAS操作
4.2.2 ABA问题

ABA问题的产生:

CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,在这个时间差内会导致数据的变化。

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant Memory as 内存位置V
    
    Note over Memory: 初始值 = A
    
    T1->>Memory: 读取值A
    T2->>Memory: 读取值A
    
    Note over T1: 线程1暂停
    
    T2->>Memory: CAS(A, B) 成功
    Note over Memory: 值变为B
    
    T2->>Memory: CAS(B, A) 成功
    Note over Memory: 值又变回A
    
    Note over T1: 线程1恢复执行
    T1->>Memory: CAS(A, C) 成功
    Note over Memory: 值变为C
    
    Note over T1,T2: 线程1的CAS成功了<br/>但不知道A值已经被修改过

ABA问题示例:

public class ABAExample {
    private AtomicReference<String> atomicRef = new AtomicReference<>("A");
    
    public void demonstrateABA() {
        // 线程1
        new Thread(() -> {
            String value = atomicRef.get();  // 读取到"A"
            System.out.println("线程1读取到: " + value);
            
            // 模拟一些处理时间
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            
            // 尝试CAS操作
            boolean success = atomicRef.compareAndSet(value, "C");
            System.out.println("线程1 CAS结果: " + success);  // 可能成功,但A值已经被改过
        }).start();
        
        // 线程2
        new Thread(() -> {
            // 快速执行A->B->A的变化
            atomicRef.compareAndSet("A", "B");
            System.out.println("线程2: A->B");
            atomicRef.compareAndSet("B", "A");
            System.out.println("线程2: B->A");
        }).start();
    }
}
4.2.3 ABA问题的解决方案

使用AtomicStampedReference,通过版本号(时间戳)来解决ABA问题:

public class ABAResolution {
    // 使用版本号解决ABA问题
    private AtomicStampedReference<String> stampedRef = 
        new AtomicStampedReference<>("A", 1);
    
    public void solveABA() {
        // 线程1
        new Thread(() -> {
            int[] stampHolder = new int[1];
            String value = stampedRef.get(stampHolder);  // 获取值和版本号
            int stamp = stampHolder[0];
            
            System.out.println("线程1读取到值: " + value + ", 版本号: " + stamp);
            
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            
            // 使用版本号进行CAS操作
            boolean success = stampedRef.compareAndSet(value, "C", stamp, stamp + 1);
            System.out.println("线程1 CAS结果: " + success);
        }).start();
        
        // 线程2
        new Thread(() -> {
            int[] stampHolder = new int[1];
            String value = stampedRef.get(stampHolder);
            int stamp = stampHolder[0];
            
            // A->B
            stampedRef.compareAndSet(value, "B", stamp, stamp + 1);
            
            // 重新获取当前状态
            value = stampedRef.get(stampHolder);
            stamp = stampHolder[0];
            
            // B->A
            stampedRef.compareAndSet(value, "A", stamp, stamp + 1);
            
            System.out.println("线程2完成A->B->A操作,最终版本号: " + (stamp + 1));
        }).start();
    }
}

版本号机制的工作原理:

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant Memory as AtomicStampedReference
    
    Note over Memory: 初始: 值=A, 版本=1
    
    T1->>Memory: 读取(值=A, 版本=1)
    T2->>Memory: 读取(值=A, 版本=1)
    
    T2->>Memory: CAS(A->B, 版本1->2)
    Note over Memory: 值=B, 版本=2
    
    T2->>Memory: CAS(B->A, 版本2->3)
    Note over Memory: 值=A, 版本=3
    
    T1->>Memory: CAS(A->C, 版本1->2)
    Memory->>T1: 失败!版本号不匹配
    
    Note over T1,T2: 版本号机制成功检测到ABA问题
AtomicStampedReference源码深度分析

AtomicStampedReference 的设计确实每次更新都创建新对象,但这正是它解决 ABA 问题的关键。下面从使用到底层源码逐步分析:

一、使用层面(解决 ABA 问题)
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 1);

// 线程1:尝试修改值
int[] stampHolder = new int[1];
String current = ref.get(stampHolder); // 返回"A",stampHolder[0]=1
// 线程1在此处暂停

// 线程2:修改两次(A->B->A)
ref.compareAndSet("A", "B", 1, 2);  // 成功
ref.compareAndSet("B", "A", 2, 3);  // 成功(值变回A,但版本号变为3)

// 线程1恢复执行:
boolean success = ref.compareAndSet(
    "A",        // 预期值仍是A
    "C",        // 新值
    stampHolder[0], // 预期版本号=1
    4           // 新版本号
); // 返回 false!因为实际版本号已是3
二、源码解析(为什么对象不同仍能判断)

1. 核心比较逻辑

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;  // 获取当前Pair对象
    
    // 第一步:比较内容而非对象地址!
    return
        expectedReference == current.reference &&  // 比较引用值
        expectedStamp == current.stamp &&          // 比较版本号值
        
        // 第二步:检查是否无需更新
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         
         // 第三步:执行CAS更新
         casPair(current, Pair.of(newReference, newStamp)));
}

2. 关键点:对象不同但内容相同

比较对象比较方式说明
Pair对象引用地址比较通过==比较对象地址
reference值比较通过==比较引用指向的对象
stamp整数值比较通过==比较基本类型值

当线程1执行CAS时:

  • expectedReference ("A")current.reference ("A") 值相同 ✓
  • expectedStamp (1)current.stamp (3) 值不同 ✗
  • → 条件失败!
三、内存模型解析
volatile Pair<V> pair;  // 关键volatile变量

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(
        this, 
        pairOffset, 
        cmp,  // 预期原对象地址
        val   // 新对象地址
    );
}

对象创建流程

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant Memory as AtomicStampedReference
    participant Pair1 as Pair@1001
    participant Pair2 as Pair@1002
    participant Pair3 as Pair@1003
    participant Pair4 as Pair@1004
    
    Note over Memory: 初始状态:pair = Pair@1001("A", 1)
    
    T1->>Memory: 读取当前pair
    Memory->>T1: 返回Pair@1001("A", 1)
    
    T2->>Memory: compareAndSet("A", "B", 1, 2)
    Memory->>Pair2: 创建Pair@1002("B", 2)
    Memory->>Memory: casPair(Pair@1001, Pair@1002)
    Note over Memory: 成功:pair指向Pair@1002
    
    T2->>Memory: compareAndSet("B", "A", 2, 3)
    Memory->>Pair3: 创建Pair@1003("A", 3)
    Memory->>Memory: casPair(Pair@1002, Pair@1003)
    Note over Memory: 成功:pair指向Pair@1003
    
    T1->>Memory: compareAndSet("A", "C", 1, 4)
    Memory->>Pair4: 创建Pair@1004("C", 4)
    Memory->>Memory: casPair(Pair@1001, Pair@1004)
    Note over Memory: 失败:当前pair是Pair@1003 ≠ 预期的Pair@1001
  1. 初始状态pair = Pair@1001(reference="A", stamp=1)

  2. 线程2第一次更新

    • casPair(Pair@1001, Pair@1002("B",2))
    • → 成功:pair指向新对象Pair@1002
  3. 线程2第二次更新

    • casPair(Pair@1002, Pair@1003("A",3))
    • → 成功:pair指向新对象Pair@1003
  4. 线程1尝试更新

    • casPair(Pair@1001, Pair@1004("C",4))
    • → 失败:当前pairPair@1003 ≠ 预期的Pair@1001
四、设计精妙之处

1. 内容比较 vs 对象比较

  • 虽然每次创建新对象,但比较的是对象内部的值(引用值+版本号),而非对象本身地址

2. 双重验证机制

  • 先验证值:确保当前状态符合预期
  • 再验证对象地址:确保期间未被修改

3. 内存可见性保证

  • volatile保证每次读取的都是最新对象,而线程本地的current是读取时的快照

4. 不可变对象(Immutable)

  • Pair被设计为不可变(final字段),一旦创建永不改变,避免并发修改
private static class Pair<T> {
    final T reference;  // 不可变引用
    final int stamp;    // 不可变版本号
    
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

5. 完整的CAS操作流程

flowchart TD
    A[compareAndSet调用] --> B[获取当前Pair对象]
    B --> C{比较reference值}
    C -->|不同| D[返回false]
    C -->|相同| E{比较stamp值}
    E -->|不同| D
    E -->|相同| F{检查是否需要更新}
    F -->|无需更新| G[返回true]
    F -->|需要更新| H[创建新Pair对象]
    H --> I[执行casPair操作]
    I --> J{CAS成功?}
    J -->|成功| G
    J -->|失败| D
    
    style D fill:#ffcccc
    style G fill:#ccffcc

这种设计确保了即使值相同,只要期间发生过修改(版本号不同),CAS操作就会失败,从而彻底解决了ABA问题。

4.3 CAS适用场景分析

graph TD
    A[CAS使用场景评估] --> B{并发程度}
    B -->|低并发| C[CAS性能优异]
    B -->|高并发| D[需要评估自旋开销]
    
    A --> E{数据竞争激烈程度}
    E -->|竞争不激烈| F[CAS效果好]
    E -->|竞争激烈| G[考虑其他方案]
    
    A --> H{是否需要避免ABA}
    H -->|不需要| I[使用普通CAS]
    H -->|需要| J[使用版本号机制]
    
    C --> K[推荐使用CAS]
    F --> K
    I --> K
    
    D --> L[性能测试验证]
    G --> M[考虑锁或其他方案]
    J --> N[使用AtomicStampedReference]

5. CAS实现机制总结

5.1 CAS的核心优势

  1. 无锁化:避免了传统锁机制的线程阻塞和上下文切换
  2. 原子性:通过硬件指令保证操作的原子性
  3. 高性能:在低竞争场景下性能优异
  4. 乐观策略:基于乐观锁思想,假设冲突较少

5.2 CAS的技术栈层次

graph TB
    A[Java应用层] --> B[AtomicInteger等原子类]
    B --> C[Unsafe类]
    C --> D[JNI本地方法]
    D --> E[C++运行时]
    E --> F[CPU cmpxchg指令]
    F --> G[硬件内存控制器]
    
    subgraph "软件层"
        A
        B
        C
        D
        E
    end
    
    subgraph "硬件层"
        F
        G
    end

5.3 CAS vs 传统锁对比

特性CAS传统锁(synchronized)
阻塞性非阻塞阻塞
性能低竞争时高性能有固定开销
实现方式硬件原子指令操作系统互斥量
线程切换可能发生
适用场景低竞争、简单操作复杂临界区、高竞争
ABA问题存在不存在
饥饿问题可能存在可通过公平锁解决

5.4 最佳实践建议

  1. 场景选择:在低竞争、简单操作场景下优先考虑CAS
  2. 性能监控:高并发场景下监控CPU使用率,避免过度自旋
  3. ABA防护:需要时使用AtomicStampedReference或AtomicMarkableReference
  4. 回退策略:在CAS性能不佳时考虑回退到传统锁机制
  5. 合理设计:避免在CAS操作中包含复杂逻辑

CAS作为现代并发编程的重要技术,在正确使用的前提下能够显著提升程序性能,但需要根据具体场景权衡其优缺点,选择最适合的并发控制策略。