1. 从锁到CAS:Java并发控制的进化之路
在Java并发编程的发展历程中,我们经历了从重量级锁到轻量级同步的显著转变。早期开发者习惯使用synchronized关键字,这种内置锁虽然简单易用,但在高并发场景下会带来严重的性能瓶颈。当多个线程竞争同一把锁时,失败的线程会被挂起并进入阻塞状态,这种线程状态切换涉及操作系统内核态与用户态的转换,每次切换可能消耗高达1微秒的时间。
实测数据:在4核i7处理器上,当100个线程通过
synchronized竞争共享资源时,吞吐量仅为约2000次操作/秒,而基于CAS的实现能达到15000次以上。
CAS(Compare-And-Swap)的出现改变了这一局面。这种无锁(lock-free)算法通过硬件指令级的原子操作实现线程安全,其核心思想是"先检查后修改"——只有当变量的当前值与预期值一致时,才会执行更新操作。现代CPU普遍支持的cmpxchg指令(如x86架构)正是CAS的硬件基础,单条指令的原子性避免了锁机制带来的上下文切换开销。
java复制// 典型的CAS操作伪代码
public boolean compareAndSwap(int expectedValue, int newValue) {
if(currentValue == expectedValue) {
currentValue = newValue;
return true;
}
return false;
}
CAS在Java中的实现主要依赖于sun.misc.Unsafe类(JDK内部API)和java.util.concurrent.atomic包。以AtomicInteger为例,其getAndIncrement()方法底层就是通过循环CAS实现的:
java复制public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe类中的实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
2. Java并发工具包中的CAS实战应用
2.1 Atomic原子类家族
java.util.concurrent.atomic包提供了一系列原子变量类,这些类都是基于CAS实现的线程安全容器:
-
基础类型:
AtomicInteger:整型原子类AtomicLong:长整型原子类AtomicBoolean:布尔型原子类
-
引用类型:
AtomicReference:对象引用原子类AtomicStampedReference:带版本号的引用(解决ABA问题)AtomicMarkableReference:带标记位的引用
-
数组类型:
AtomicIntegerArray:整型数组原子类AtomicLongArray:长整型数组原子类AtomicReferenceArray:引用类型数组原子类
这些类的共同特点是都提供了compareAndSet方法,其典型使用模式如下:
java复制AtomicInteger counter = new AtomicInteger(0);
// 线程安全的自增操作
public void safeIncrement() {
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
2.2 并发容器中的CAS应用
Java并发容器的高性能很大程度上得益于CAS的巧妙运用:
-
ConcurrentHashMap:
- JDK8之前使用分段锁,之后改为CAS+synchronized组合
- 关键操作如
putVal中通过CAS初始化桶节点 - 统计大小采用
LongAdder机制(CAS变种)
-
CopyOnWriteArrayList:
- 写操作时通过CAS保证只有一个线程能创建新数组
- 读操作完全无锁,直接访问volatile数组引用
-
ConcurrentLinkedQueue:
- 完全基于CAS实现的无界非阻塞队列
- 头尾节点更新都通过CAS保证原子性
java复制// ConcurrentHashMap中的关键CAS代码片段
Node<K,V>[] tab = table;
Node<K,V> f = tabAt(tab, i = (n - 1) & hash);
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
2.3 同步工具类中的CAS
-
CountDownLatch:
- 内部计数器使用CAS更新
countDown()方法实质是CAS递减操作
-
Semaphore:
- 许可证数量通过CAS原子更新
- 非公平模式直接尝试CAS获取许可
-
AQS(AbstractQueuedSynchronizer):
- 所有同步器的基石
- 状态变量state使用volatile+CAS保证可见性与原子性
- 入队出队操作通过CAS保证线程安全
3. CAS的典型问题与解决方案
3.1 ABA问题及其应对
ABA问题是CAS操作中的经典陷阱:假设变量初始值为A,线程1准备将其改为C,但在读取A后,线程2将A改为B又改回A。此时线程1执行CAS时仍会成功,但中间状态变化可能带来隐患。
解决方案:
- 版本号机制:
AtomicStampedReference通过增加stamp版本号标记状态变化 - 布尔标记:
AtomicMarkableReference使用布尔值标记对象是否被修改过
java复制// 使用AtomicStampedReference解决ABA问题
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String current = ref.get(stampHolder);
ref.compareAndSet(current, "C", stampHolder[0], stampHolder[0]+1);
3.2 自旋开销与优化
当竞争激烈时,CAS失败会导致大量无效的自旋操作,消耗CPU资源。针对这种情况有以下优化策略:
- 指数退避:每次失败后增加等待时间
- 队列化:如AQS将等待线程放入队列有序唤醒
- 自适应自旋:JVM根据历史成功率动态调整自旋次数
实测建议:当CAS失败率超过50%时,应考虑改用锁机制或调整并发策略。
3.3 伪共享与缓存行填充
由于CPU缓存以缓存行为单位(通常64字节),当多个原子变量位于同一缓存行时,一个变量的修改会导致整个缓存行失效,这种现象称为伪共享(False Sharing)。
解决方案:
- 手动填充:通过添加无意义字段使对象占满缓存行
- @Contended注解:JDK8引入的自动填充方式(需开启-XX:-RestrictContended)
java复制// 手动填充示例
public class PaddedAtomicLong extends AtomicLong {
public volatile long p1, p2, p3, p4, p5, p6; // 填充字段
public PaddedAtomicLong() { super(); }
// ... 其他方法
}
4. 从JVM到硬件:CAS的底层实现剖析
4.1 JVM层面的CAS实现
HotSpot虚拟机中,CAS操作主要通过Unsafe类提供的方法实现:
-
本地方法调用:
compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong
-
内存语义:
- 具有volatile读/写的内存可见性
- 保证原子性的处理器指令
-
方法内联:
- 关键CAS方法会被JIT编译器内联优化
- 消除方法调用开销
4.2 硬件指令支持
不同CPU架构对CAS的实现方式:
| 架构 | 指令 | 特性 |
|---|---|---|
| x86 | CMPXCHG | 支持8/16/32/64位比较交换 |
| ARM | LDREX/STREX | 加载-存储独占对实现原子操作 |
| POWER | LARX/STCX | 类似的加载保留/条件存储 |
assembly复制// x86的cmpxchg指令示例
lock cmpxchg [mem], reg
4.3 JVM参数调优
与CAS性能相关的关键JVM参数:
- -XX:+UseSpinWait:启用自旋等待(默认开启)
- -XX:PreBlockSpin=10:控制自旋次数(JDK6之前)
- -XX:+UseBiasedLocking:偏向锁优化(可能影响CAS性能)
- -XX:+UseCompressedOops:压缩指针(影响对象头CAS)
5. 高级应用:手写基于CAS的并发组件
5.1 实现线程安全栈
java复制public class ConcurrentStack<E> {
private AtomicReference<Node<E>> top = new AtomicReference<>();
private static class Node<E> {
final E item;
Node<E> next;
public Node(E item) {
this.item = item;
}
}
public void push(E item) {
Node<E> newHead = new Node<>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null) return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
}
5.2 设计无锁队列
java复制public class LockFreeQueue<E> {
private static class Node<E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item) {
this.item = item;
this.next = new AtomicReference<>(null);
}
}
private final AtomicReference<Node<E>> head;
private final AtomicReference<Node<E>> tail;
public LockFreeQueue() {
Node<E> dummy = new Node<>(null);
head = new AtomicReference<>(dummy);
tail = new AtomicReference<>(dummy);
}
public void enqueue(E item) {
Node<E> newNode = new Node<>(item);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// 帮助其他线程完成尾节点更新
tail.compareAndSet(curTail, tailNext);
} else {
if (curTail.next.compareAndSet(null, newNode)) {
tail.compareAndSet(curTail, newNode);
return;
}
}
}
}
}
public E dequeue() {
while (true) {
Node<E> oldHead = head.get();
Node<E> oldTail = tail.get();
Node<E> next = oldHead.next.get();
if (oldHead == head.get()) {
if (oldHead == oldTail) {
if (next == null) return null;
tail.compareAndSet(oldTail, next);
} else {
E item = next.item;
if (head.compareAndSet(oldHead, next)) {
return item;
}
}
}
}
}
}
5.3 性能优化技巧
-
消除热点变量:
- 将频繁修改的变量拆分为多个
- 如
LongAdder采用分段累计策略
-
批处理操作:
- 合并多个CAS操作为单次操作
- 如
getAndAdd比单独get+compareAndSet更高效
-
延迟更新策略:
- 非关键路径上的更新可适当延迟
- 减少CAS竞争概率
-
线程本地化:
- 使用
ThreadLocal减少共享变量访问 - 定期同步到主存
- 使用
6. 生产环境中的CAS实践心得
在实际项目中应用CAS时,有几个关键经验值得分享:
-
监控CAS失败率:
- 通过JMX或自定义计数器统计CAS失败情况
- 失败率超过30%应考虑调整算法
-
避免过度优化:
- 简单的
synchronized在低竞争时可能更高效 - 使用
jstack和性能分析工具验证实际效果
- 简单的
-
内存屏障的正确使用:
- CAS本身包含完整的内存屏障
- 混合使用时注意
volatile与CAS的交互
-
测试策略:
- 必须进行高并发压力测试
- 特别关注边界条件和长时间运行稳定性
-
与JVM版本的兼容性:
- 不同JDK版本对CAS的优化策略不同
- 如JDK8对
AtomicLong的优化不如LongAdder
java复制// 性能对比示例:AtomicLong vs LongAdder
AtomicLong atomicCounter = new AtomicLong();
LongAdder adderCounter = new LongAdder();
// 高并发场景下
IntStream.range(0, 100).parallel().forEach(i -> {
atomicCounter.incrementAndGet(); // 性能较差
adderCounter.increment(); // 性能更好
});
在最近的一个电商平台项目中,我们使用CAS实现了分布式环境下的库存预扣减系统。最初采用简单的AtomicInteger,但在大促期间发现CAS竞争激烈导致性能下降。后来改造为分段CAS(类似ConcurrentHashMap的设计),将库存拆分为16个槽位,根据用户ID哈希分配到不同槽位,使吞吐量提升了8倍。这个案例让我深刻体会到:CAS虽好,但必须根据具体场景设计合适的竞争规避策略。
