学完这篇,你能说清 CAS 到底在比较什么、AtomicInteger 靠什么保证线程安全,以及什么时候该改用 LongAdder,并且能手写一个基于 CAS 的自旋锁。
第一步:先看普通的 i++ 为什么会丢数据
public class Demo {
static int count = 0;
public static void main(String[] args) throws Exception {
Thread[] ts = new Thread[10];
for (int i = 0; i < 10; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) count++;
});
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println(count); // 大概率小于 100000
}
}
count++ 在字节码里是三条指令:读 count → 加 1 → 写回。两个线程可能同时读到 0,各自算出 1 再写回,其中一次自增就丢了。这是典型的「读-改-写」竞态。想看到字节码,运行 javap -c Demo.class | grep -A3 iinc,或者用 -XX:+PrintAssembly(需要 hsdis 插件)看机器指令。
第二步:CAS 的三个值
CAS(Compare And Swap)是一条 CPU 原子指令,它需要三个值:
- V:内存中的当前值
- A:你期望的旧值
- B:想写入的新值
语义是:只有 V 等于 A 时,才把 V 改成 B 并返回 true;否则什么都不做返回 false。判断和写入在这个指令里是一次性完成的,中间不会被打断。
Java 里最终落到 sun.misc.Unsafe 的几个 native 方法上:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
在 x86 上编译出来就是带 lock 前缀的 cmpxchg,lock 前缀同时承担了内存屏障的作用,所以 CAS 成功时既保证原子性也保证可见性。
第三步:拆开 AtomicInteger.incrementAndGet()
JDK 8 的源码很短:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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;
}
valueOffset 是 value 字段在对象内存中的偏移量,在静态代码块里通过 unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")) 拿到。value 字段本身也被 volatile 修饰,保证读到的是最新值。
关键在 do-while:CAS 失败说明有人抢先改了,那就重新读一遍再试,这就是所谓的「自旋」。整个过程不加锁、不让线程阻塞,并发不高时比 synchronized 轻得多。
注意:从 JDK 9 开始 Unsafe 的部分方法被标记废弃,JDK 11+ 想要反射拿 theUnsafe 需要加 --add-opens java.base/sun.misc=ALL-UNNAMED,否则会抛 InaccessibleObjectException。新代码直接用 VarHandle 或 AtomicInteger 的公开 API,别再依赖 Unsafe。
第四步:自己用 CAS 写一个自旋锁
理解了原理就能手写:
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread cur = Thread.currentThread();
while (!owner.compareAndSwap(null, cur)) {
// 抢不到就空转,可以加 Thread.onSpinWait() 提示 CPU
}
}
public void unlock() {
owner.compareAndSet(Thread.currentThread(), null);
}
}
compareAndSwap(null, cur) 的意思是「如果当前没人持锁,就把锁给我」。抢失败就原地循环再抢。这个锁只适合临界区极短的场景。
注意:自旋锁在单核机器或不释放 CPU 的场景下会白白烧 CPU。JDK 9+ 建议在循环里加 Thread.onSpinWait(),它会给处理器一个忙等提示,减少流水线冲刷。生产环境请优先用 ReentrantLock,不要自己造锁。
第五步:三个必须知道的坑
1. ABA 问题。 线程 1 读到值 A,线程 2 把 A 改成 B 又改回 A,线程 1 的 CAS 依然成功,但它并不知道中间发生过变化。对栈、链表这类结构会出大问题。解决办法是加版本号:
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int stamp = ref.getStamp();
ref.compareAndSet("A", "B", stamp, stamp + 1); // 值 + 版本号一起比
2. 自旋开销。 高并发下 CAS 大量失败,大量线程空转,CPU 飙高。看线程状态可以 jstack <pid> | grep -c RUNNABLE,如果 RUNNABLE 数量异常高且 CPU 打满,就要怀疑自旋重试。
3. 只能保证一个变量的原子性。 AtomicInteger 只管一个 int。多个变量要一起原子更新,用 AtomicReference 把它们封装成一个不可变对象,整体替换。
第六步:高并发计数选 LongAdder
AtomicLong 在几千个线程同时自增时,所有线程抢同一个内存地址,CAS 失败率极高。LongAdder 的做法是分段累加:内部维护一个 base 和一个 Cell[] 数组,不同线程哈希到不同的 Cell 上各自累加,最后 sum() 把所有 Cell 加起来。
LongAdder adder = new LongAdder();
adder.increment();
long total = adder.sum();
选型口诀:**并发写少、要求强实时读,用 AtomicLong;高并发写、只偶尔读一次总数,用 LongAdder。**
注意:LongAdder.sum() 返回的不是原子快照,累加过程中如果有线程正在写,结果可能略偏。它适合统计场景(QPS、访问量),不适合当唯一序列号或余额。
小结
- CAS 靠 CPU 的
cmpxchg 指令完成「比较 + 交换」,Java 层通过 Unsafe 的 native 方法暴露出来。
AtomicInteger.incrementAndGet() = 读 volatile 值 + CAS 失败自旋重试,不加锁。
valueOffset 是字段内存偏移,value 是 volatile,两者缺一不可。
- ABA 用
AtomicStampedReference 加版本号解决;多变量用 AtomicReference 封装。
- 高并发计数优先
LongAdder,但 sum() 不是强一致快照。
- 别自己造锁,也别在新代码里直接依赖
Unsafe。