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

CAS运行机制详解

1. CAS的出现背景

1.1 没有CAS之前的问题

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

java 复制代码
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++操作,类似乐观锁的机制:

java 复制代码
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. 返回步骤:返回操作是否成功的结果

伪代码表示

java 复制代码
// 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的核心类,它提供了直接操作内存的能力:

java 复制代码
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()实现

java 复制代码
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方法分析

java 复制代码
// 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使用率飙升:

java 复制代码
// 高并发场景下的性能问题示例
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成功了
但不知道A值已经被修改过

ABA问题示例:

java 复制代码
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问题:

java 复制代码
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 问题)
java 复制代码
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. 核心比较逻辑

java 复制代码
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) 值不同 ✗
  • → 条件失败!
三、内存模型解析
java 复制代码
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字段),一旦创建永不改变,避免并发修改
java 复制代码
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作为现代并发编程的重要技术,在正确使用的前提下能够显著提升程序性能,但需要根据具体场景权衡其优缺点,选择最适合的并发控制策略。

相关推荐
非ban必选2 分钟前
spring-ai-alibaba官方 Playground 示例
java·人工智能·spring
一粒沙白猫4 分钟前
Java综合练习04
java·开发语言·算法
凌辰揽月12 分钟前
8分钟讲完 Tomcat架构及工作原理
java·架构·tomcat
笑醉踏歌行18 分钟前
idea应用代码配色网站
java·ide·intellij-idea
一入JAVA毁终身20 分钟前
处理Lombok的一个小BUG
java·开发语言·bug
gjh120832 分钟前
Easy-excel监听器中对批量上传的工单做错误收集
java·spring boot
红衣女妖仙35 分钟前
JXLS 库导出复杂 Excel
java·excel·jxls·java 导出 excel
绝无仅有40 分钟前
对接三方SDK开发过程中的问题排查与解决
后端·面试·架构
Hellyc42 分钟前
JAVA八股文:异常有哪些种类,可以举几个例子吗?Throwable类有哪些常见方法?
java·开发语言
要开心吖ZSH43 分钟前
《Spring 中上下文传递的那些事儿》Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理
java·spring