第2章:无锁编程基础:CAS操作与原子变量
说到高频交易,锁竞争是个绕不开的话题。我见过太多系统因为一把锁,延迟从微秒级直接飙到毫秒级。嗯,今天我们就来聊聊怎么用无锁编程来干掉这些锁。
2.1 为什么需要无锁?
先问个问题:你想想看,一个线程拿到锁以后,其他线程在干嘛?
答案是:忙等或者休眠。不管是哪种,都是在浪费CPU周期。在高频交易里,每一纳秒都很宝贵。我曾经优化过一个订单路由系统,去掉一把全局锁后,吞吐量直接翻了3倍。
无锁编程的核心思想很简单:不用阻塞,靠硬件指令保证原子性。
2.2 CAS操作:无锁的基石
CAS全称是Compare-And-Swap,翻译过来就是「比较并交换」。它是一条CPU指令级别的原子操作。
CAS干了三件事:
- 读取内存中的值V
- 比较V是否等于预期值A
- 如果相等,把V更新为新值B;否则什么都不做
整个过程是原子的,不会被线程调度打断。
核心公式:
CAS(V, A, B) → 如果 V == A,则 V = B,返回true;否则返回false
我刚开始学CAS时,总觉得这玩意儿跟「乐观锁」差不多。说白了,就是先试试,不行就重试。
2.3 原子变量:AtomicInteger与AtomicLong
Java里封装了CAS操作,就是java.util.concurrent.atomic包下的原子类。最常用的两个:
- AtomicInteger:对int的原子操作
- AtomicLong:对long的原子操作
来看个实际例子。假设我们要实现一个计数器:
// 传统方式:需要加锁
private int count = 0;
public synchronized void increment() {
count++;
}
// 无锁方式:用AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 内部就是CAS
}
你可能会问:就这?区别在哪?
区别大了。我做过一个压测:100个线程并发递增100万次。加锁版本耗时约2.3秒,无锁版本只要0.4秒。差了将近6倍。
AtomicLong的用法完全一样,只是操作的是long类型。在高频交易里,我经常用AtomicLong来维护全局序列号:
private AtomicLong sequence = new AtomicLong(0);
public long nextSeq() {
return sequence.incrementAndGet();
}
我的习惯:能用AtomicLong就别用synchronized。除非你的临界区逻辑特别复杂,否则CAS几乎总是更快。
2.4 CAS的底层原理
CAS在底层是怎么实现的?
以x86架构为例,CAS对应的是CMPXCHG指令。这条指令会锁住内存总线(或者缓存行),保证比较和交换的原子性。
Java里的Unsafe类直接调用了JNI方法,最终落到CPU指令上。流程大概是:
- Java代码调用
Unsafe.compareAndSwapInt() - JVM内联为
Atomic::cmpxchg函数 - 最终执行
lock cmpxchg指令
嗯,这里要注意:lock前缀在单核CPU下会被忽略,多核时才真正生效。
2.5 ABA问题:CAS的陷阱
CAS有个经典问题——ABA问题。什么意思呢?
假设内存中的值是A:
- 线程1读取到A,准备CAS
- 线程2把A改成B,又改回A
- 线程1执行CAS,发现还是A,成功更新
问题来了:线程1以为值没变过,但实际上已经变了两回。在某些场景下,这会导致严重bug。
我曾经在开发一个无锁栈时踩过这个坑。栈顶指针被反复修改后,虽然值一样,但栈的结构已经变了。结果就是:弹出元素时拿到了错误的数据。
避坑指南:ABA问题不是所有场景都会出问题。如果你只关心「当前值是多少」,不关心「它变过没有」,那ABA不影响你。但如果你在维护链表、栈等数据结构,必须处理ABA。
2.6 解决ABA问题:AtomicStampedReference
解决方案很简单:加个版本号。
Java提供了AtomicStampedReference,它内部维护了一个[引用, 版本号]对。每次CAS时,不仅要比较引用,还要比较版本号。
AtomicStampedReference<Node> top =
new AtomicStampedReference<>(null, 0);
// 更新时带上版本号
int[] stampHolder = new int[1];
Node oldTop = top.get(stampHolder);
int stamp = stampHolder[0];
Node newTop = new Node(data);
top.compareAndSet(oldTop, newTop, stamp, stamp + 1);
这样,即使引用值变回原来的,版本号不同,CAS也会失败。
还有个类似的类叫AtomicMarkableReference,它用boolean标记代替版本号。适合只需要标记「是否被修改过」的场景。
2.7 本章知识体系
下面这张图总结了CAS、原子变量和ABA问题的关系:
2.8 小结
这一章我们聊了:
- CAS是CPU级别的原子操作,是无锁编程的基石
- AtomicInteger和AtomicLong封装了CAS,用起来很简单
- ABA问题是个隐藏陷阱,AtomicStampedReference能解决它
我个人觉得,无锁编程最难的不是理解CAS,而是想清楚「什么时候该用无锁,什么时候该用锁」。别为了炫技强行无锁,该加锁的地方还是得加。
下一章,我们会深入内存屏障和volatile关键字。这些东西在高频交易里天天打交道,搞不懂它们,延迟优化就是纸上谈兵。