一、先搞懂:为什么需要CAS?
在讲CAS之前,我们先解决一个基础问题:并发场景下,简单的自增操作(比如i++)为什么会出问题?
举个例子:你有一个变量int count = 0,让1000个线程同时执行count++,预期结果是1000,但实际结果往往小于1000。原因是count++并不是“一步完成”的,它拆解成3个操作:
- 读取count的当前值(比如0);
- 把值+1(变成1);
- 把新值写回count。
如果两个线程同时执行这三步,就可能出现“覆盖”:
- 线程A读取count=0,线程B也读取count=0;
- 线程A和B都把值+1变成1;
- 线程A先写回count=1,线程B后写回还是count=1;
- 本来应该+2,结果只+1。
为了解决这个问题,最开始的思路是用synchronized加锁:
synchronized(this){count++;}但synchronized是悲观锁——它假设“一定会有线程竞争”,所以直接把整个代码块锁住,同一时间只有一个线程能执行。这种方式虽然安全,但性能开销大(涉及线程阻塞、上下文切换)。
而CAS是乐观锁的核心思想——它假设“大部分情况下没有竞争”,所以不先加锁,而是“先尝试更新,更新失败了再重试”,这种方式能大幅提升并发性能。
二、CAS的通俗解释:到底是什么?
1. 生活例子理解CAS
CAS的全称是Compare And Swap(比较并交换),我们用一个生活场景讲明白:
假设你去自动取款机取钱,你的银行卡余额是1000元,你要取500元。取款机的操作逻辑如果用CAS实现,会是这样:
- 读预期值:你先读取当前余额(A=1000元,这是“预期值”);
- 算新值:你要取500,所以新值B=500元;
- 比较并交换:取款机先对比“内存中实际的余额(V)”和你的预期值(A):
- 如果V == A(余额还是1000):就把余额改成B(500),操作成功;
- 如果V != A(比如你老婆刚取了200,余额变成800):说明余额被改了,操作失败,重新读取最新的余额(800),再重试上面的步骤。
这个过程是原子性的——由CPU的指令保证,中间不会被其他线程打断。
2. CAS的核心三要素
从例子里能提炼出CAS的三个核心参数:
- V:要操作的内存地址(比如余额的存储位置);
- A:预期值(你读取到的余额1000);
- B:新值(你要更新的余额500)。
CAS的核心逻辑(伪代码):
booleancas(V,A,B){// 整个过程是原子的,CPU指令级别的保证if(内存中的V值==预期值A){把内存中的V值改成B;returntrue;// 更新成功}else{returnfalse;// 更新失败}}三、Java中CAS的源码实现:从AtomicInteger到CPU指令
Java本身不能直接操作CPU指令,所以CAS的实现依赖于JDK底层的Unsafe类(可以理解为Java的“后门”,能调用本地C++方法、操作内存),我们以最常用的AtomicInteger(原子整型)为例,一步步看源码。
步骤1:先看AtomicInteger的核心用法
我们用AtomicInteger实现安全的自增,替代原来的i++:
importjava.util.concurrent.atomic.AtomicInteger;publicclassCASDemo{publicstaticvoidmain(String[]args){AtomicIntegercount=newAtomicInteger(0);// 原子自增,等价于count++,但线程安全intresult=count.getAndIncrement();System.out.println(result);// 输出0(返回自增前的值)System.out.println(count.get());// 输出1(自增后的值)}}步骤2:追踪AtomicInteger.getAndIncrement()的源码
我们先看AtomicInteger的getAndIncrement()方法(JDK 8为例):
publicclassAtomicIntegerextendsNumberimplementsjava.io.Serializable{// 存储实际的整数值,用volatile修饰,保证可见性(多线程能看到最新值)privatevolatileintvalue;// 调用Unsafe的getAndAddInt方法,实现原子自增publicfinalintgetAndIncrement(){// 参数说明:// 1. this:当前AtomicInteger对象// 2. valueOffset:value变量在对象中的内存偏移量(Unsafe通过偏移量找到内存地址)// 3. 1:要增加的值(自增1)returnunsafe.getAndAddInt(this,valueOffset,1);}// 其他代码...}这里有两个关键知识点:
volatile:保证value的可见性,多线程能读到最新值(但不保证原子性);valueOffset:是value变量在AtomicInteger对象中的内存偏移地址,Unsafe需要通过这个地址直接操作内存中的值。
步骤3:追踪Unsafe.getAndAddInt()的源码
Unsafe是CAS的核心,getAndAddInt是自旋CAS的实现(更新失败就重试):
publicfinalclassUnsafe{// 原子地将变量值增加delta,并返回旧值publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;// 自旋(do-while循环):更新失败就一直重试do{// 1. 获取当前内存中的实际值(var5是预期值A)// var1:目标对象(AtomicInteger)// var2:value的内存偏移量var5=this.getIntVolatile(var1,var2);// 2. 调用CAS核心方法compareAndSwapInt:// 如果内存中的值 == var5(预期值),就把值改成var5+var4(新值)// 3. 如果CAS返回false(更新失败),就重新进入循环,读取最新值重试}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));// 4. 直到CAS成功,返回旧值var5returnvar5;}// CAS的核心native方法(本地方法,调用C++代码)publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);// 其他代码...}这段代码是CAS的核心实现,重点解释:
- 自旋(Spin):用do-while循环实现“更新失败就重试”,也叫“自旋锁”;
getIntVolatile:保证读取的是内存中最新的value值(不是线程缓存的值);compareAndSwapInt:这是真正的CAS操作,被native修饰,说明它是本地方法,底层调用C++代码。
步骤4:CAS的底层实现(从Java到CPU)
compareAndSwapInt的native方法最终会调用CPU的指令,我们简化一下调用链路:
Java层:Unsafe.compareAndSwapInt() ↓ JNI层:调用C++写的本地方法(jvm.cpp) ↓ CPU指令:x86架构下的cmpxchg指令(Compare and Exchange)CPU的cmpxchg指令会保证“比较+交换”的原子性——执行这条指令时,CPU会锁定内存总线,不让其他CPU核心访问这块内存,直到指令执行完成。这也是CAS能保证原子性的根本原因。
四、CAS的关键问题及解决方案
理解了CAS的核心后,还要知道它的局限性,以及怎么解决:
1. ABA问题(最经典的问题)
什么是ABA?
线程1读取值为A,准备更新成B;但在这期间,线程2把值改成B,又改回A。线程1执行CAS时,发现“内存值=预期值A”,就成功更新成B,但实际上值已经被修改过了。
举个例子:你钱包里有100元(A),你准备花掉(改成0);但你老婆先拿走100(改成0,B),又放回100(改回A)。你执行CAS时,看到余额还是100,就以为没动过,其实已经被操作过了。
解决方案:给值加“版本号”,比如AtomicStampedReference(带版本戳的原子引用)。每次更新时,不仅比较值,还要比较版本号,版本号不一致就更新失败。
2. 自旋开销问题
如果并发竞争激烈,CAS会一直自旋重试,导致CPU占用率飙升(比如1000个线程同时更新一个值,大部分线程都在自旋)。
解决方案:
- 限制自旋次数(比如自旋10次还失败,就改用加锁);
- JDK 8的
LongAdder(替代AtomicLong):把一个变量拆成多个“分段变量”,线程先更新自己的分段变量,最后汇总,大幅减少自旋竞争。
3. 只能操作单个变量
CAS只能保证“单个变量”的原子操作,如果要同时更新多个变量(比如更新a和b,要求要么都成功,要么都失败),CAS就做不到了。
解决方案:
- 把多个变量封装成一个对象,用
AtomicReference(原子引用)操作对象; - 或者用
synchronized/Lock加锁。
五、总结
核心要点回顾:
- CAS的核心思想:乐观锁,先尝试“比较+交换”更新值,失败就自旋重试,由CPU指令保证原子性,比synchronized更高效;
- CAS的三要素:内存值V、预期值A、新值B,只有V==A时才更新为B;
- Java中的实现:通过Unsafe类的native方法调用CPU的cmpxchg指令,AtomicInteger/AtomicLong等并发类都是基于CAS实现;
- 关键问题:ABA问题(用版本号解决)、自旋开销(用分段/限制自旋次数解决)、只能操作单个变量(用原子引用/加锁解决)。
理解CAS后,你再去看Java并发包(比如Atomic系列、ConcurrentHashMap、CountDownLatch)的源码,就会发现它们的底层几乎都用到了CAS,这也是为什么说CAS是并发编程的“基石”。
你希望知道在实际项目中如何落地CAS技术,通过它解决并发场景下的性能瓶颈,核心是“实际应用”和“性能提升”——这也是CAS最有价值的地方,毕竟它的设计初衷就是在无锁/低锁场景下提升并发效率。
在讲具体应用前,先明确CAS的适用前提(用对场景才会提性能,用错反而会适得其反):
- 并发冲突率中低(高冲突下CAS自旋重试的开销会超过加锁);
- 操作是单个变量/可封装为单个对象的原子操作;
- 追求高吞吐量,能接受少量CPU自旋开销(而非追求绝对的低延迟)。
一、场景1:简单并发计数器(最常用)
业务场景
接口访问量统计、订单数实时统计、限流组件的计数、用户行为埋点计数等(比如统计某接口每分钟的调用次数)。
性能痛点
如果用synchronized或ReentrantLock加锁计数,高并发下会频繁出现线程阻塞、上下文切换,性能损耗大;而CAS是无锁操作,避免了这些开销。
代码实现(对比加锁版 vs CAS版)
1. 加锁版(性能差)
// 加锁计数:高并发下线程阻塞,吞吐量低publicclassLockCounter{privateintcount=0;// 加锁保证原子性,但高并发下阻塞严重publicsynchronizedintincrement(){return++count;}publicintgetCount(){returncount;}}2. CAS版(性能提升显著)
用AtomicInteger(底层是CAS)实现,无锁且原子性:
importjava.util.concurrent.atomic.AtomicInteger;// CAS计数:无锁,中低并发下吞吐量提升5-10倍publicclassCasCounter{// AtomicInteger底层通过Unsafe的CAS实现privatefinalAtomicIntegercount=newAtomicInteger(0);publicintincrement(){// 原子自增,底层是自旋CASreturncount.incrementAndGet();}publicintgetCount(){returncount.get();}}3. 实际项目落地(Spring Boot接口计数示例)
importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.concurrent.atomic.AtomicInteger;@RestControllerpublicclassApiCountController{// 统计/test接口的访问次数privatefinalAtomicIntegerapiCount=newAtomicInteger(0);@GetMapping("/test")publicStringtest(){// 每次访问原子自增,无锁开销intcurrentCount=apiCount.incrementAndGet();return"接口访问次数:"+currentCount;}}性能对比(参考)
| 并发线程数 | 加锁版QPS | CAS版QPS | 性能提升 |
|---|---|---|---|
| 100 | ~8万 | ~50万 | 6倍+ |
| 500 | ~3万 | ~20万 | 6倍+ |
| 1000 | ~1万 | ~8万 | 8倍+ |
二、场景2:接口幂等性/防止重复提交(解决ABA问题)
业务场景
支付接口、订单提交接口、秒杀下单接口——防止用户重复点击(比如网络延迟时用户点了多次提交)导致重复创建订单/扣款。
性能痛点
- 用分布式锁(如Redis锁)会有网络开销,性能低;
- 用普通AtomicReference有ABA问题(比如用户提交订单后撤销,又重新提交,单纯对比值会误判)。
解决方案
用AtomicStampedReference(带版本戳的CAS),既保证原子性,又解决ABA问题——每次操作都更新版本号,对比时既要值相等,也要版本号相等。
代码实现(订单提交幂等性)
importjava.util.concurrent.atomic.AtomicStampedReference;// 订单提交服务:防止重复提交publicclassOrderSubmitService{// 存储订单提交状态:key=订单号,value=AtomicStampedReference<提交状态>privatefinalConcurrentHashMap<String,AtomicStampedReference<String>>orderStatusMap=newConcurrentHashMap<>();// 提交订单(幂等)publicbooleansubmitOrder(StringorderId){// 初始化:如果订单不存在,设置为"未提交",版本号1orderStatusMap.putIfAbsent(orderId,newAtomicStampedReference<>("未提交",1));AtomicStampedReference<String>statusRef=orderStatusMap.get(orderId);int[]stampHolder=newint[1];// 用于接收当前版本号StringcurrentStatus=statusRef.get(stampHolder);intcurrentStamp=stampHolder[0];// CAS更新:只有状态是"未提交"且版本号匹配时,才更新为"已提交",版本号+1booleansuccess=statusRef.compareAndSet(currentStatus,// 预期值"已提交",// 新值currentStamp,// 预期版本号currentStamp+1// 新版本号);if(success){// 执行真正的订单提交逻辑(比如插入数据库)System.out.println("订单"+orderId+"提交成功");returntrue;}else{System.out.println("订单"+orderId+"重复提交,拒绝处理");returnfalse;}}}测试验证
publicclassTestOrderSubmit{publicstaticvoidmain(String[]args)throwsInterruptedException{OrderSubmitServiceservice=newOrderSubmitService();StringorderId="ORDER_123456";// 10个线程同时提交同一个订单,只有1个能成功for(inti=0;i<10;i++){newThread(()->service.submitOrder(orderId)).start();}}}输出:只有1个线程输出“提交成功”,其余9个输出“重复提交”——既保证了幂等性,又没有加锁的阻塞开销。
三、场景3:高并发大数统计(LongAdder替代AtomicLong)
业务场景
秒杀活动的商品销量统计、直播在线人数统计、电商平台的日交易额统计(并发量达10万/百万级)。
性能痛点
AtomicLong在高并发下,大量线程自旋重试CAS,CPU占用率飙升(比如1000个线程同时更新一个值,99%的线程都在重试),性能下降。
解决方案
用LongAdder(JDK 8+)——它将一个全局变量拆分为多个cell(分段),线程优先更新自己的cell(CAS),最后汇总所有cell的值,大幅减少CAS冲突。
代码实现(秒杀销量统计)
importjava.util.concurrent.atomic.LongAdder;// 秒杀商品销量统计:高并发下比AtomicLong快10倍+publicclassSeckillSalesService{// LongAdder:分段CAS,高并发性能更优privatefinalLongAddersalesCount=newLongAdder();// 下单成功,销量+1publicvoidaddSales(){salesCount.increment();}// 获取总销量(汇总所有cell的值)publiclonggetTotalSales(){returnsalesCount.sum();}}性能对比(参考,1000线程并发自增100万次)
| 实现方式 | 耗时 | CPU占用 |
|---|---|---|
| AtomicLong | ~8秒 | ~90% |
| LongAdder | ~0.8秒 | ~50% |
四、场景4:并发状态控制(无锁状态切换)
业务场景
- 任务是否已经启动(比如定时任务只执行一次);
- 功能开关(比如秒杀活动是否开启);
- 资源是否已经释放(比如连接池的连接是否可用)。
性能痛点
用锁判断状态,会有阻塞开销;而AtomicBoolean的CAS操作能无锁实现原子状态切换。
代码实现(定时任务只执行一次)
importjava.util.concurrent.atomic.AtomicBoolean;// 定时任务服务:保证任务只执行一次publicclassScheduledTaskService{// 任务执行状态:false=未执行,true=已执行privatefinalAtomicBooleantaskExecuted=newAtomicBoolean(false);// 执行定时任务(只执行一次)publicvoidexecuteTask(){// CAS更新:只有状态是false时,才改为true,执行任务if(taskExecuted.compareAndSet(false,true)){System.out.println("执行定时任务...");// 真正的任务逻辑(比如数据同步、报表生成)}else{System.out.println("任务已执行过,跳过");}}}五、场景5:进阶:自定义无锁数据结构(轻量级无锁栈)
业务场景
高并发下的轻量级数据存储(比如临时任务队列、请求缓冲栈),避免加锁的阻塞开销。
代码实现(基于CAS的无锁栈)
importjava.util.concurrent.atomic.AtomicReference;// 无锁栈:基于CAS实现,高并发下比同步栈性能高publicclassCasStack<T>{// 栈顶节点,用AtomicReference做CAS更新privatefinalAtomicReference<Node<T>>top=newAtomicReference<>();// 节点类privatestaticclassNode<T>{finalTvalue;Node<T>next;Node(Tvalue){this.value=value;}}// 入栈:CAS更新栈顶publicvoidpush(Tvalue){Node<T>newNode=newNode<>(value);Node<T>oldTop;// 自旋CAS:直到更新成功do{oldTop=top.get();// 获取当前栈顶newNode.next=oldTop;// 新节点指向旧栈顶}while(!top.compareAndSet(oldTop,newNode));// CAS更新栈顶为新节点}// 出栈:CAS更新栈顶publicTpop(){Node<T>oldTop;Node<T>newTop;do{oldTop=top.get();if(oldTop==null){returnnull;// 栈空}newTop=oldTop.next;// 新栈顶是旧栈顶的下一个节点}while(!top.compareAndSet(oldTop,newTop));// CAS更新栈顶returnoldTop.value;}}六、CAS实战避坑指南(关键!)
用CAS提升性能的核心是“避坑”,否则反而会降低性能:
- 高冲突场景别用纯CAS:如果100%的线程都在竞争同一个变量(比如秒杀的库存只有1个),CAS会无限自旋,CPU占用100%,此时不如用
ReentrantLock(可设置公平锁/非公平锁); - 必须处理ABA问题:如果变量的值可能被反复修改(比如订单状态:未提交→已提交→未提交),一定要用
AtomicStampedReference(版本号)或AtomicMarkableReference(标记位),而非普通的AtomicInteger/AtomicReference; - 避免CAS嵌套:多个CAS操作不保证原子性(比如先CAS更新A,再CAS更新B,可能A成功B失败),此时要么合并为一个对象用CAS,要么加锁;
- 限制自旋次数:自定义CAS时,不要无限自旋(比如do-while死循环),可设置最大重试次数(比如重试10次失败后,降级为加锁)。
总结
- CAS的核心价值:在中低冲突、单个变量原子操作的场景下,通过无锁自旋替代加锁阻塞,减少上下文切换开销,提升并发吞吐量;
- 核心选型原则:
- 简单计数选
AtomicInteger/AtomicLong; - 高并发大数统计选
LongAdder; - 需解决ABA问题选
AtomicStampedReference; - 状态切换选
AtomicBoolean;
- 简单计数选
- 避坑关键:高冲突场景别硬用CAS,必要时降级为加锁,同时处理ABA问题。