在Java并发编程领域,除了synchronized这种基于锁的同步机制外,还有一种更轻量级的方案——CAS(Compare-And-Swap,比较并交换)。CAS是无锁编程的核心思想,而JUC(java.util.concurrent)包下的原子类则是CAS思想的最佳实践。本文将从CAS的底层原理出发,拆解JUC原子类的设计、解决的问题(如ABA)、性能优化手段,以及CAS的应用场景和局限性,帮助你掌握无锁并发的核心技术。
一、CAS:无锁编程的基石
1.1 什么是CAS?
CAS是一种原子指令(由CPU硬件层面保证原子性),其核心思想是:在更新变量值之前,先比较变量当前值是否与预期值一致;如果一致,就将变量更新为新值;如果不一致,就放弃更新并重试。
与synchronized的"悲观锁"思维(假设一定会有线程竞争,先加锁再执行)不同,CAS是典型的乐观锁思维:假设不会有线程竞争,直接尝试更新;只有发现竞争时,才通过重试(自旋)解决问题,全程无需加锁。
1.2 CAS的核心操作步骤
CAS操作包含三个核心参数:
V:要更新的变量(内存地址);A:预期值(线程认为变量当前应该有的值);B:新值(线程想要将变量更新为的值)。
完整执行流程:
- 线程读取变量
V的当前值,记为当前值; - 判断
当前值是否等于预期值A:- 如果相等:将变量
V的值更新为新值B,操作成功; - 如果不相等:说明有其他线程修改了变量,放弃更新,重新执行步骤1(自旋重试);
- 如果相等:将变量
- 重复上述过程,直到操作成功(或达到重试阈值后放弃)。
1.3 CAS的Java实现示例
Java中通过Unsafe类(提供直接操作内存的能力)调用CAS指令,以下是简化的CAS操作示例:
importsun.misc.Unsafe;importjava.lang.reflect.Field;publicclassCASDemo{// 要更新的共享变量privatevolatileintvalue=0;privatestaticfinalUnsafeunsafe;privatestaticfinallongvalueOffset;// 静态代码块获取Unsafe实例和value字段的内存偏移量static{try{// 通过反射获取Unsafe实例(Unsafe的构造方法是私有的)FieldunsafeField=Unsafe.class.getDeclaredField("theUnsafe");unsafeField.setAccessible(true);unsafe=(Unsafe)unsafeField.get(null);// 获取value字段在CASDemo对象中的内存偏移量FieldvalueField=CASDemo.class.getDeclaredField("value");valueOffset=unsafe.objectFieldOffset(valueField);}catch(Exceptione){thrownewRuntimeException(e);}}// 自定义CAS操作publicbooleancompareAndSwap(intexpect,intupdate){// Unsafe的compareAndSwapInt方法:CAS核心实现// 参数:对象实例、字段偏移量、预期值、新值returnunsafe.compareAndSwapInt(this,valueOffset,expect,update);}// 自旋CAS实现自增publicvoidincrement(){intcurrent;do{// 获取当前值(预期值)current=this.value;// 尝试CAS更新:预期值是current,新值是current+1}while(!compareAndSwap(current,current+1));}publicintgetValue(){returnvalue;}publicstaticvoidmain(String[]args)throwsInterruptedException{CASDemodemo=newCASDemo();// 启动10个线程,每个线程自增1000次for(inti=0;i<10;i++){newThread(()->{for(intj=0;j<1000;j++){demo.increment();}}).start();}// 等待所有线程执行完成Thread.sleep(1000);System.out.println("最终值:"+demo.getValue());// 输出10000,线程安全}}1.4 CAS与synchronized的性能对比
| 特性 | CAS | synchronized |
|---|---|---|
| 锁类型 | 乐观锁(无锁) | 悲观锁(有锁) |
| 线程阻塞 | 无阻塞(自旋重试) | 可能阻塞(重量级锁) |
| 适用场景 | 高并发、低冲突 | 高并发、高冲突 |
| 性能开销 | 冲突低时开销极小 | 始终有锁的获取/释放开销 |
| 资源消耗 | 冲突高时CPU自旋消耗大 | 冲突高时线程阻塞,CPU消耗低 |
核心结论:
- 高并发、低冲突场景(如少量线程更新共享变量):CAS更优,无锁切换开销,性能远超
synchronized; - 高并发、高冲突场景(如大量线程争抢同一个变量):CAS会导致大量线程空自旋,CPU利用率飙升,此时
synchronized(重量级锁)更稳定(线程阻塞,避免无效自旋)。
二、JUC原子类:CAS的开箱即用实现
JDK提供了java.util.concurrent.atomic包,封装了各种基于CAS的原子类,无需我们手动实现CAS自旋,直接解决共享变量的线程安全问题。
2.1 JUC原子类的分类
JUC原子类主要分为四大类,覆盖不同类型的共享变量操作:
2.1.1 基本类型原子类
AtomicInteger:原子更新int类型;AtomicLong:原子更新long类型;AtomicBoolean:原子更新boolean类型。
核心方法(以AtomicInteger为例):
AtomicIntegercount=newAtomicInteger(0);count.getAndIncrement();// 先获取值,再自增(i++)count.incrementAndGet();// 先自增,再获取值(++i)count.getAndAdd(5);// 先获取值,再加5count.compareAndSet(0,1);// CAS操作:预期值0,新值12.1.2 数组类型原子类
AtomicIntegerArray:原子更新int数组的元素;AtomicLongArray:原子更新long数组的元素;AtomicReferenceArray:原子更新引用类型数组的元素。
示例:
int[]arr={1,2,3};AtomicIntegerArrayatomicArr=newAtomicIntegerArray(arr);// 原子更新索引0的元素:预期值1,新值10atomicArr.compareAndSet(0,1,10);System.out.println(atomicArr.get(0));// 输出102.1.3 引用类型原子类
AtomicReference:原子更新引用类型(解决多个共享变量的原子性问题);AtomicStampedReference:解决ABA问题的引用类型(带版本号);AtomicMarkableReference:解决ABA问题的引用类型(带标记)。
2.1.4 字段更新器原子类
AtomicIntegerFieldUpdater:原子更新对象的int字段;AtomicLongFieldUpdater:原子更新对象的long字段;AtomicReferenceFieldUpdater:原子更新对象的引用字段。
特点:可以原子更新对象的非静态字段(要求字段必须是volatile修饰的)。
2.2 CAS的经典问题:ABA问题
2.2.1 什么是ABA问题?
ABA问题是CAS的天然缺陷:线程1准备更新变量值时,发现变量当前值是A(预期值),但在此期间,变量可能被线程2修改为B,又被线程3改回A。此时线程1的CAS操作会认为变量未被修改,成功更新,但实际上变量已经被修改过,可能导致逻辑错误。
示例场景:
- 线程1读取变量
x=A,准备CAS更新为C; - 线程2抢占CPU,将
x从A改为B; - 线程3又将
x从B改回A; - 线程1恢复执行,发现
x=A(与预期值一致),执行CAS将x改为C。
线程1的CAS操作成功,但x实际上被修改过两次,这就是ABA问题。
2.2.2 ABA问题的解决方案
核心思路:为变量增加"版本号"或"标记",不仅比较值,还要比较版本号/标记。
方案1:版本号(AtomicStampedReference)
AtomicStampedReference为每个变量绑定一个版本号(stamp),CAS操作时同时比较"值"和"版本号",只有两者都匹配时才更新。
示例:
importjava.util.concurrent.atomic.AtomicStampedReference;publicclassABADemo{publicstaticvoidmain(String[]args){// 初始化:值为A,版本号为1AtomicStampedReference<String>asr=newAtomicStampedReference<>("A",1);// 线程1:准备CAS更新为C,先获取当前值和版本号StringexpectValue=asr.getReference();intexpectStamp=asr.getStamp();System.out.println("线程1-预期值:"+expectValue+",预期版本号:"+expectStamp);// 线程2:模拟ABA操作newThread(()->{// 版本号+1,将A改为Basr.compareAndSet("A","B",asr.getStamp(),asr.getStamp()+1);System.out.println("线程2-将A改为B,版本号:"+asr.getStamp());// 版本号+1,将B改回Aasr.compareAndSet("B","A",asr.getStamp(),asr.getStamp()+1);System.out.println("线程2-将B改回A,版本号:"+asr.getStamp());}).start();// 等待线程2执行完成try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}// 线程1执行CAS:值是A,但版本号已经变了(从1变为3)booleanresult=asr.compareAndSet(expectValue,"C",expectStamp,expectStamp+1);System.out.println("线程1-CAS结果:"+result);// 输出falseSystem.out.println("最终值:"+asr.getReference()+",最终版本号:"+asr.getStamp());}}输出结果:
线程1-预期值:A,预期版本号:1 线程2-将A改为B,版本号:2 线程2-将B改回A,版本号:3 线程1-CAS结果:false 最终值:A,最终版本号:3线程1的CAS操作失败,因为版本号不匹配,成功解决ABA问题。
方案2:标记位(AtomicMarkableReference)
AtomicMarkableReference为变量绑定一个布尔标记(mark),用于标记变量"是否被修改过",适用于只需判断"是否修改",无需记录版本号的场景。
示例:
importjava.util.concurrent.atomic.AtomicMarkableReference;publicclassAtomicMarkableDemo{publicstaticvoidmain(String[]args){// 初始化:值为A,标记为false(未被修改)AtomicMarkableReference<String>amr=newAtomicMarkableReference<>("A",false);// 线程2修改值newThread(()->{// 将A改为B,标记为true(已修改)amr.compareAndSet("A","B",false,true);// 将B改回A,标记保持true(已修改)amr.compareAndSet("B","A",true,true);}).start();try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}// 线程1尝试CAS:预期值A,预期标记falsebooleanresult=amr.compareAndSet("A","C",false,true);System.out.println("CAS结果:"+result);// 输出false}}2.3 高并发下CAS的性能优化
CAS的核心问题是:竞争激烈时,大量线程空自旋,导致CPU利用率飙升,性能下降。JDK针对这个问题提供了两种优化思路:
2.3.1 空间换时间:LongAdder(替代AtomicLong)
AtomicLong在高并发下,所有线程争抢同一个变量的CAS操作,导致自旋次数剧增。LongAdder的核心优化是:
- 将一个全局变量拆分为多个局部变量(cell数组);
- 线程更新时,先通过哈希算法定位到某个cell,更新该cell的值;
- 读取全局值时,累加所有cell的值 + 基值(base)。
原理示意图:
LongAdder { base: 0 // 基础值,竞争小时直接更新base cells: [cell1, cell2, cell3...] // 竞争大时,分散到不同cell }使用示例:
importjava.util.concurrent.atomic.LongAdder;publicclassLongAdderDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{LongAdderadder=newLongAdder();// 启动100个线程,每个线程自增10000次for(inti=0;i<100;i++){newThread(()->{for(intj=0;j<10000;j++){adder.increment();// 原子自增}}).start();}Thread.sleep(2000);System.out.println("最终值:"+adder.sum());// 输出1000000}}核心优势:
- 低竞争时:直接更新base,性能与
AtomicLong持平; - 高竞争时:分散到多个cell,减少CAS冲突,性能远超
AtomicLong。
2.3.2 队列削峰:CLH队列(AQS核心)
另一种优化思路是:将争抢CAS的线程加入队列排队,避免同时自旋。典型代表是AQS(AbstractQueuedSynchronizer),其核心逻辑:
- 线程尝试CAS获取资源,如果失败;
- 将线程封装为节点,加入CLH队列尾部;
- 队列中的线程不再空自旋,而是通过"前驱节点通知"的方式唤醒,减少CPU消耗。
这种方式本质是"将并发争抢转为串行排队",降低CAS冲突的激烈程度,是ReentrantLock、CountDownLatch等JUC工具的底层实现。
三、CAS的应用场景与局限性
3.1 CAS的核心应用
3.1.1 JUC原子类
如AtomicInteger、LongAdder等,是CAS最直接的应用,解决基本类型/引用类型的原子更新问题。
3.1.2 AQS(抽象队列同步器)
AQS是JUC并发工具的基石(ReentrantLock、Semaphore、CountDownLatch等都基于AQS),其核心是通过CAS操作更新同步状态(state变量),并通过CLH队列管理等待线程。
3.1.3 ConcurrentHashMap
JDK1.8的ConcurrentHashMap放弃了分段锁,改用CAS +synchronized实现并发安全:
- 初始化桶(Node数组)时,用CAS保证原子性;
- 插入元素时,先尝试CAS更新,失败后再用
synchronized锁定桶节点; - 扩容时,用CAS标记扩容状态,避免多线程重复扩容。
3.2 CAS的局限性
3.2.1 ABA问题
如前文所述,可通过AtomicStampedReference/AtomicMarkableReference解决。
3.2.2 只能保证单个变量的原子性
CAS只能对一个共享变量执行原子操作;如果需要同时更新多个共享变量,可通过以下方式解决:
- 将多个变量封装为一个对象,用
AtomicReference原子更新对象引用; - 使用锁(
synchronized/ReentrantLock)保证多个变量操作的原子性。
示例:AtomicReference更新多个变量
importjava.util.concurrent.atomic.AtomicReference;// 封装多个变量为对象classUser{privateintid;privateStringname;publicUser(intid,Stringname){this.id=id;this.name=name;}// 省略getter/setter}publicclassAtomicReferenceDemo{publicstaticvoidmain(String[]args){Useruser1=newUser(1,"张三");Useruser2=newUser(2,"李四");AtomicReference<User>atomicRef=newAtomicReference<>(user1);// 原子更新整个User对象(同时更新id和name)booleanresult=atomicRef.compareAndSet(user1,user2);System.out.println(result);// 输出trueSystem.out.println(atomicRef.get().getId()+":"+atomicRef.get().getName());// 输出2:李四}}3.2.3 无效自旋导致CPU开销大
高并发、高冲突场景下,大量线程自旋重试CAS操作,会导致CPU利用率达到100%,系统响应变慢。解决方案:
- 限制自旋次数(如AQS默认自旋10次后加入队列);
- 使用
LongAdder替代AtomicLong; - 冲突极高时,改用
synchronized(重量级锁)。
3.2.4 总线风暴
CAS是CPU的原子指令,执行时会锁定总线(MESI协议),大量CAS操作会导致总线带宽被占满,其他CPU核心无法正常通信,即"总线风暴"。解决方案:减少CAS操作的频率(如批量更新、分散竞争)。
四、总结
核心知识点回顾
- CAS的本质:乐观锁思想,通过CPU原子指令实现"比较-交换",无锁但可能自旋,适用于高并发低冲突场景。
- JUC原子类:CAS的开箱即用实现,分为基本类型、数组类型、引用类型、字段更新器四类,解决共享变量的原子更新问题。
- ABA问题:CAS的经典缺陷,可通过版本号(
AtomicStampedReference)或标记位(AtomicMarkableReference)解决。 - CAS性能优化:高并发下用
LongAdder(空间换时间)或队列排队(AQS)减少CAS冲突,避免空自旋。 - CAS的局限性:存在ABA问题、仅支持单个变量原子性、高冲突时CPU开销大、可能引发总线风暴。
实战建议
- 低并发场景:优先使用
AtomicInteger/AtomicLong,简单高效; - 高并发计数场景:用
LongAdder替代AtomicLong,大幅提升性能; - 需解决ABA问题:使用
AtomicStampedReference(记录版本)或AtomicMarkableReference(记录标记); - 多变量原子更新:用
AtomicReference封装对象,或直接使用锁; - 极高冲突场景:放弃CAS,改用
synchronized(JDK1.6后优化,性能已大幅提升)。
CAS和JUC原子类是Java无锁并发的核心,掌握其原理和使用场景,能让你在高并发编程中灵活选择同步方案,既保证线程安全,又最大化系统性能。