面试必知必会(5):CAS与原子类

面试必知必会(5):CAS与原子类

Java面试系列文章

面试必知必会(1):线程状态和创建方式

面试必知必会(2):线程池原理

面试必知必会(3):synchronized底层原理

面试必知必会(4):volatile关键字

面试必知必会(5):CAS与原子类


目录

引言

在高并发编程领域,线程安全是永恒的核心议题。传统的悲观锁机制(如synchronized、ReentrantLock)通过阻塞线程实现资源互斥,虽能保证安全性,但会带来上下文切换、线程唤醒等性能开销。而CAS(Compare-And-Swap,比较并交换)作为乐观锁的核心实现,以无锁方式实现原子操作,成为高并发场景下提升性能的关键技术。。

一、什么是CAS

  • CAS(Compare-And-Swap),即比较并交换,是一种无锁的并发控制技术
  • 包含三个操作数
    • 内存位置(V):需要更新的共享变量在内存中的地址
    • 预期原值(A):线程读取到的变量当前值,即期望变量保持的值
    • 目标新值(B):变量需要更新到的最终值

CAS的操作流程可概括为:读取内存位置V的当前值,若该值与预期原值A一致,则将V更新为新值B并返回成功;若不一致,则说明变量已被其他线程修改,放弃本次更新并返回失败

// 伪代码表示CAS操作booleancompareAndSwap(MemoryAddress V,ExpectedValue A,NewValue B){if(V == A){ V = B;returntrue;}returnfalse;}

其核心思想是“乐观假设无冲突”——默认不会有其他线程同时修改变量,仅在冲突发生时通过重试(而非阻塞)规避问题,从而避免了锁机制的性能损耗。

二、CAS的底层实现

CAS的原子性无法通过纯软件实现,必须依赖硬件层面的原子指令支持,整个依赖链路分为三层。

1、CPU原子指令支撑

  • 不同架构的CPU提供了专门的原子指令来实现CAS操作:
    • x86架构:提供cmpxchg(Compare and Exchange)指令,该指令可在一条指令内完成“比较-交换”的原子操作
    • ARM架构:通过ldrex(加载并标记)和strex(存储并检查标记)指令对实现,标记期间其他线程无法修改目标内存地址,确保操作原子性
  • 这些指令保证了操作的原子性,即使在多核处理器环境下也不会被中断。

2、Unsafe类:Java的底层“魔法工具”

Java并未直接暴露CPU指令,而是通过sun.misc.Unsafe类提供的native方法封装底层操作。Unsafe类是JVM内部使用的“危险类”,允许直接操作内存地址和调用底层指令,其提供的CAS相关方法是原子类的核心依赖:

public final classUnsafe{// 比较并交换int值public final native booleancompareAndSwapInt(Object o,long offset,int expected,int x);// 比较并交换long值public final native booleancompareAndSwapLong(Object o,long offset,long expected,long x);// 比较并交换Object值public final native booleancompareAndSwapObject(Object o,long offset,Object expected,Object x);}

这些方法通过native关键字调用C++内联汇编代码,最终触发对应CPU架构的原子指令。需要注意的是,Unsafe类不建议普通开发者直接使用,其操作无安全校验,可能引发内存越界等严重问题。

3、volatile关键字:保证内存可见性

  • CAS操作依赖变量的实时可见性,否则线程可能读取到过期值。原子类中的共享变量均通过volatile修饰
    • 可见性:一个线程对变量的修改会立即刷新到主内存,其他线程读取时直接从主内存获取,避免缓存一致性问题
    • 禁止指令重排:确保变量操作的顺序与代码逻辑一致,避免CAS操作被重排导致的逻辑错乱

三、CAS的核心问题与解决方案

1、ABA问题:值的“伪不变”陷阱

ABA问题是CAS最经典的逻辑漏洞:变量从A被修改为B,随后又被改回A,CAS操作会认为“值未变化”而成功执行,但中间的状态变更可能导致依赖变量状态的逻辑出错。

  • (链表操作中的ABA风险)假设单向链表初始状态为A→B,两个线程同时执行“删除头节点”操作
    • Thread1读取头节点为A、下一个节点为B,准备执行CAS更新头节点为B,但被阻塞
    • Thread2成功删除头节点A,链表变为B;随后新增节点A,链表变为A→B(新A节点)
    • Thread1恢复运行,CAS检查头节点仍为A,执行更新将头节点设为B,错误删除新A节点的下一个节点B,导致链表结构损坏

解决方案:不仅校验变量值,还校验其“版本”,确保值的变化轨迹可追溯。Java中提供两类原子类实现该方案。

  • AtomicStampedReference(版本戳记引用):为对象引用绑定一个int类型的版本号(stamp),每次修改时同时更新值和版本号(版本号递增)。CAS操作需同时匹配“值”和“版本号”才会成功,即使值回滚,版本号也会变化,从而避免ABA问题
// 初始化:值为"A",版本号为1AtomicStampedReference<String> asr =newAtomicStampedReference<>("A",1);// 获取当前值和版本号int oldStamp = asr.getStamp();// 1String oldValue = asr.getReference();// "A"// 仅当“值为A且版本号为1”时,更新为"B"并将版本号设为2boolean success = asr.compareAndSet(oldValue,"B", oldStamp, oldStamp +1);

2、自旋开销问题:高冲突下的性能损耗

CAS失败时会通过“自旋”(循环重试CAS操作),若并发冲突剧烈,大量线程会持续自旋,占用CPU资源,导致性能下降。

解决方案:

  • 自适应自旋:JVM可根据历史自旋成功率动态调整自旋次数,成功率高则增加次数,反之减少
  • 分段锁优化:如LongAdder(后续原子类部分详解),将变量拆分为多个分段,线程仅操作对应分段,降低冲突概率
  • 冲突阈值控制:当自旋次数超过阈值时,降级为阻塞锁,避免CPU空耗
自旋锁:线程拿不到锁就循环重试,无上下文切换开销,但消耗 CPU。适合短耗时操作
阻塞锁:线程拿不到锁就休眠,不消耗 CPU,但有巨大的上下文切换开销。适合长耗时操作

3、只能保证单个变量的原子操作

CAS仅能对单个变量实现原子操作,无法直接保证多个变量组合操作的原子性(如“更新变量a同时更新变量b”)。

解决方案:

  • 将多个变量封装为一个对象,通过AtomicReference原子更新对象引用,间接实现组合操作的原子性
  • 必要时使用锁机制(如ReentrantLock)包裹多个CAS操作,牺牲部分性能换取组合原子性

二、Java原子类:CAS的上层封装与实战

Java并发包(java.util.concurrent.atomic)提供了一系列原子类,基于sun.misc.Unsafe类封装了常用原子操作,无需开发者直接操作底层指令。根据操作目标类型,原子类可分为四大类。

1、基本类型原子类

  • 针对intlongboolean三种基本类型,提供对应的原子类,核心方法包括自增自减CAS更新
    • AtomicInteger:原子更新int类型值
    • AtomicLong:原子更新long类型值
    • AtomicBoolean:原子更新boolean类型值(底层通过int类型转换实现,0表示false,1表示true)

AtomicInteger - 原子更新int类型

importjava.util.concurrent.atomic.AtomicInteger;publicclassAtomicIntegerExample{publicstaticvoidmain(String[] args){// 创建AtomicInteger实例,初始值为0AtomicInteger atomicInt =newAtomicInteger(0);// 基本操作示例System.out.println("初始值: "+ atomicInt.get());// 输出: 0// 设置新值 atomicInt.set(10);System.out.println("设置后: "+ atomicInt.get());// 输出: 10// 原子递增 atomicInt.incrementAndGet();// i++System.out.println("incrementAndGet后: "+ atomicInt.get());// 输出: 11 atomicInt.getAndIncrement();// ++iSystem.out.println("getAndIncrement后: "+ atomicInt.get());// 输出: 12// 原子递减 atomicInt.decrementAndGet();// i--System.out.println("decrementAndGet后: "+ atomicInt.get());// 输出: 11 atomicInt.getAndDecrement();// --iSystem.out.println("getAndDecrement后: "+ atomicInt.get());// 输出: 10// 原子加法 atomicInt.addAndGet(5);// 加5并返回新值System.out.println("addAndGet(5)后: "+ atomicInt.get());// 输出: 15int oldValue = atomicInt.getAndAdd(3);// 加3但返回旧值System.out.println("getAndAdd(3) - 旧值: "+ oldValue +", 当前值: "+ atomicInt.get());// 输出: 旧值:15, 当前值:18// 原子更新(CAS操作)boolean success = atomicInt.compareAndSet(18,20);// 如果当前值是18,则更新为20System.out.println("CAS操作成功: "+ success +", 当前值: "+ atomicInt.get());// 输出: true, 20// 获取并更新int newValue = atomicInt.updateAndGet(x -> x *2);// 对当前值进行运算并返回新值System.out.println("updateAndGet(x->x*2)结果: "+ newValue);// 输出: 40}}

核心源码解析(以AtomicInteger为例)

publicclassAtomicInteger{// 获取Unsafe实例privatestaticfinalUnsafe unsafe =Unsafe.getUnsafe();// value字段的内存偏移量(用于直接定位内存地址)privatestaticfinallong valueOffset;// 共享变量,volatile保证可见性privatevolatileint value;static{try{// 计算value字段在类中的内存偏移量 valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));}catch(Exception ex){thrownewError(ex);}}// CAS核心方法:预期值与当前值一致则更新publicfinalbooleancompareAndSet(int expect,int update){return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}// 原子自增(返回旧值)publicfinalintgetAndIncrement(){// 自旋CAS:失败则重新读取并重试return unsafe.getAndAddInt(this, valueOffset,1);}// 原子自增(返回新值)publicfinalintincrementAndGet(){returngetAndAddInt(1)+1;}}

其中,Unsafe.getAndAddInt方法底层通过do-while循环实现自旋

publicfinalclassUnsafe{// 以 volatile 语义读取对象 o中偏移量为 offset的 int 类型字段的值,并返回这个 int 值publicnativeintgetIntVolatile(Object o,long offset);// int类型原子操作比较并交换publicfinalnativebooleancompareAndSwapInt(Object o,long offset,int expected,int x);publicfinalintgetAndAddInt(Object o,long offset,int delta){int v;do{// 以volatile方式读取当前值(保证可见性) v =getIntVolatile(o, offset);}while(!compareAndSwapInt(o, offset, v, v + delta));// 失败则重试return v;}}

compareAndSwapInt工作原理(伪代码)

booleancompareAndSwapInt(Object o,long offset,int expected,int x){// 1. 计算字段的实际内存地址int* address =(int*)((char*)o + offset);// 2. 读取当前值int current =*address;// 3. 比较当前值与期望值if(current == expected){// 4. 如果相等,则写入新值(原子操作)*address = x;returntrue;// 成功}else{returnfalse;// 失败}}
⚠️ 注意:上面的伪代码 不是原子的!真正的 compareAndSwapInt是由 CPU 在硬件层面保证整个操作(比较 + 交换)是原子的,不会被线程调度打断

2、数组类型原子类

  • 针对数组元素的原子操作,提供三类原子类,支持通过索引原子更新数组中的元素,底层同样依赖CAS机制
    • AtomicIntegerArray:原子更新int数组元素
    • AtomicLongArray:原子更新long数组元素
    • AtomicReferenceArray:原子更新引用类型数组元素
// 初始化int数组,所有元素原子更新AtomicIntegerArray array =newAtomicIntegerArray(newint[]{1,2,3});// 原子更新索引0的元素为5(仅当当前值为1时) array.compareAndSet(0,1,5);// 原子自增索引1的元素 array.incrementAndGet(1);System.out.println(array.get(0));// 5System.out.println(array.get(1));// 3

3、对象引用类型原子类

针对对象引用的原子操作,除了前文解决ABA问题的AtomicStampedReference(比较引用值+版本戳),还包括基础的AtomicReference(比较引用值)AtomicMarkableReference(比较引用值+标记位),用于原子更新对象引用本身。

  • AtomicReference:基本的引用原子更新
classData{privateString content;// 构造器、getter、setter省略}publicclassAtomicReferenceDemo{privatestaticfinalAtomicReference<Data> cacheRef =newAtomicReference<>(newData("初始缓存"));// 原子更新缓存publicstaticvoidupdateCache(Data newData){ cacheRef.compareAndSet(cacheRef.get(), newData);}publicstaticvoidmain(String[] args){Data newCache =newData("更新后缓存");updateCache(newCache);System.out.println(cacheRef.get().getContent());// 更新后缓存}}
  • AtomicMarkableReference:为对象引用绑定一个boolean类型的标记(mark),仅表示“值是否被修改过”,不记录修改次数
// 初始化:值为"A",标记为false(未修改)AtomicMarkableReference<String&gt; amr =newAtomicMarkableReference<>("A",false);boolean[] markHolder =newboolean[1];// 用于接收当前标记String oldValue = amr.get(markHolder);// 获取值,markHolder[0]为false// 仅当“值为A且标记为false”时,更新为"B"并将标记设为trueboolean success = amr.compareAndSet(oldValue,"B",false,true);

4、对象字段更新原子类

  • 针对对象的指定字段(非静态字段)实现原子更新,无需修改字段所属类的代码,灵活性极高
  • 需满足两个条件:字段必须是volatile修饰的(保证可见性),且不能是private字段(封装性保护)
    • AtomicIntegerFieldUpdater:原子更新对象的volatile int字段
    • AtomicLongFieldUpdater:原子更新对象的volatile long字段
    • AtomicReferenceFieldUpdater:原子更新对象的volatile引用字段
publicclassBankAccount{// 普通属性privateString name;// 正确做法:private + volatileprivatevolatileint balance;privatestaticfinalAtomicIntegerFieldUpdater<BankAccount> updater =AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"balance");// 修改balance值publicvoiddeposit(int amount){// 只能通过原子操作更新 updater.addAndGet(this, amount);}// 自增balance值publicintincrement(){ updater.incrementAndGet(this);}// 提供安全的读取方法publicintgetBalance(){return updater.get(this);}}

5、性能优化原子类:LongAdder与DoubleAdder

在高并发计数场景下,AtomicLong的自旋CAS会因冲突剧烈导致性能下降。JDK 1.8引入LongAdderDoubleAdder,通过“分段锁”思想优化性能。

核心原理

  • LongAdder将计数器拆分为多个“单元格”(Cell数组),每个线程优先操作自己对应的单元格(通过线程哈希定位),降低CAS冲突概率
  • 最终获取结果时,累加所有单元格的值与基础值(base)
  • 当并发量较低时,直接操作base值,与AtomicLong性能接近
  • 并发量较高时,单元格分散冲突,性能远超AtomicLong

实战对比:LongAdder与AtomicLong性能

publicclassLongAdderVsAtomicLong{privatestaticfinalAtomicLong atomicLong =newAtomicLong();privatestaticfinalLongAdder longAdder =newLongAdder();privatestaticfinalint THREAD_COUNT =20;privatestaticfinalint LOOP_COUNT =1000000;publicstaticvoidmain(String[] args)throwsInterruptedException{// 测试AtomicLong性能ExecutorService executor1 =Executors.newFixedThreadPool(THREAD_COUNT);long start1 =System.currentTimeMillis();for(int i =0; i < THREAD_COUNT; i++){ executor1.submit(()->{for(int j =0; j < LOOP_COUNT; j++){ atomicLong.incrementAndGet();}});} executor1.shutdown(); executor1.awaitTermination(1,TimeUnit.MINUTES);long cost1 =System.currentTimeMillis()- start1;System.out.println("AtomicLong耗时:"+ cost1 +"ms,结果:"+ atomicLong.get());// 测试LongAdder性能ExecutorService executor2 =Executors.newFixedThreadPool(THREAD_COUNT);long start2 =System.currentTimeMillis();for(int i =0; i < THREAD_COUNT; i++){ executor2.submit(()->{for(int j =0; j < LOOP_COUNT; j++){ longAdder.increment();}});} executor2.shutdown(); executor2.awaitTermination(1,TimeUnit.MINUTES);long cost2 =System.currentTimeMillis()- start2;System.out.println("LongAdder耗时:"+ cost2 +"ms,结果:"+ longAdder.sum());}}

运行结果通常显示:高并发下LongAdder耗时仅为AtomicLong的1/3~1/2,适合海量并发计数场景

三、CAS与原子类的适用场景与对比

1、适用场景

  • 低冲突、短耗时操作:如计数器、状态标志位更新,CAS无锁特性可最大化性能
  • 高并发计数/累加:优先使用LongAdder/DoubleAdder,分段锁设计适配高冲突场景
  • 无锁数据结构实现:如无锁队列、无锁栈,基于AtomicReference封装CAS操作,提升并发吞吐量
  • 对象字段/引用的原子更新:使用字段更新原子类或引用类型原子类,无需修改原有类结构,灵活性高

2、与悲观锁(synchronized/ReentrantLock)的对比

对比维度CAS(乐观锁)/原子类悲观锁
核心思想"先操作,后检测冲突"(假设冲突很少发生)"先加锁,再操作"​(假设冲突总发生)
线程行为无阻塞,冲突时自旋重试竞争失败时线程挂起(上下文切换)
性能开销轻量级,没有锁竞争和唤醒开销重量级,含上下文切换、锁调度开销
冲突处理高冲突下自旋耗CPU,性能下降高冲突下稳定性更好,阻塞避免CPU空耗
功能范围仅支持单个变量/字段的原子操作支持任意代码块的原子性,功能更全面
ABA问题存在,需额外机制解决不存在,互斥访问避免中间状态变更