上一期文章内容:Java并发中的乐观锁与悲观锁,
本期文章我们来讲一下Java并发中的CAS机制
一、从银行账户案例理解CAS
CAS 是一种乐观锁机制,用于在不使用锁的情况下实现多线程对共享资源的并发访问。
它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。
当且仅当内存位置 V 的值等于预期原值 A 时,才将内存位置 V 的值更新为新值 B;
否则,不做任何操作。整个 CAS 操作是原子性的,由 CPU 硬件指令直接支持。
想象这样一个场景:你和朋友同时查看一个银行账户余额为100元,你们都尝试转账。传统做法是银行用"锁"机制,只允许一人操作。而CAS(Compare-And-Swap比较交换)采用更聪明的策略:
你查询当前余额:100元(旧值)
系统记录此刻的版本号为V1
当你提交转账时,系统会检查:
当前余额是否仍是100元?
版本号是否还是V1?
只有当两者都满足时,转账才会成功,并更新版本号为V2
这种无锁机制就像超市自助结账——不需要收银员(锁),顾客(线程)自己完成操作,系统通过版本检查保证安全。
二、Java底层实现揭秘
2.1 神秘的Unsafe类
Java通过sun.misc.Unsafe
类实现CAS,这个类就像Java世界的"瑞士军刀",为什么会这么说呢?
因为 sun.misc.Unsafe 类功能强大并且多样,提供了直接操作内存、基于CAS的操作方法、线程调度相关方法等的能力,但是我们在使用时需要持谨慎态度,因为会涉及安全、可移植、代码维护等问题!
java">public final class Unsafe {// 对象类型CASpublic final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);// int类型CASpublic final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);// long类型CASpublic final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
}
通过JNI调用本地代码,最终映射到CPU指令(如x86的CMPXCHG
),整个过程就像:
获取当前值
计算新值
调用CPU指令进行原子比较交换
2.2 AtomicInteger实现解析
以AtomicInteger
为例,看Java如何包装CAS:
java">public class AtomicInteger {private volatile int value; // 保证可见性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); }}public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
}
关键点说明:
volatile
保证值变更的可见性内存偏移量定位字段位置
CAS失败时自动重试(自旋)
三、CAS的三大挑战与应对
3.1 ABA问题:账户余额的"时空穿越"
假设账户余额变化:100 → 200 → 100
线程A读取100(版本V1)
线程B完成两次修改(V1→V2→V3)
线程A的CAS检查值仍是100,但版本已变化
解决方案: 使用带版本号的AtomicStampedReference
java">AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0);// 存款操作
int[] stampHolder = new int[1];
int current = account.get(stampHolder);
if(account.compareAndSet(current, current+50, stampHolder[0], stampHolder[0]+1)) {System.out.println("存款成功");
}
3.2 自旋开销:CPU的"空转危机"
当多个线程激烈竞争时,CAS可能导致CPU空转。优化策略:
-
自适应自旋:JVM动态调整自旋次数
-
pause指令:插入CPU提示指令降低功耗
// x86架构实现示例 __asm__ volatile ("pause");
-
退避策略:随机等待后再重试
3.3 多变量原子性:关联操作的困局
需要保证多个变量的原子更新时:
方案一:对象包装
class Account {int balance;int version;
}AtomicReference<Account> atomicAccount = new AtomicReference<>();
方案二:锁机制
synchronized(lock) {account.balance -= amount;account.version++;
}
四、CAS应用场景分析
场景 | 适用性 | 示例 |
---|---|---|
计数器 | ★★★★★ | AtomicInteger |
状态标志 | ★★★★★ | AtomicBoolean |
对象引用更新 | ★★★★☆ | AtomicReference |
复杂数据结构 | ★★☆☆☆ | ConcurrentHashMap内部实现 |
事务性操作 | ★☆☆☆☆ | 需要结合其他机制 |
五、性能对比:CAS vs 锁
通过JMH基准测试(ops/ms):
线程数 | CAS | 同步锁 | ReentrantLock |
1 | 15234 | 14567 | 14230 |
4 | 12345 | 2345 | 4566 |
8 | 9876 | 1023 | 2348 |
结论:
低竞争场景:性能相近
高并发场景:CAS性能优势明显
极端竞争:可能需要退化为锁机制