AtomicInteger
AtomicInteger 源码分析
无锁的原子 int:i++ 这种”读-改-写”复合操作靠 CPU 的 CAS/fetch-and-add 指令一步完成,不加锁也线程安全。整个类几乎没有逻辑,全是往 Unsafe 上转发——真正值得看的是它怎么定位内存、怎么自旋、以及 JIT 怎么把自旋整个优化掉。
代码块收起展开
// 基于 JDK 17 本地源码, java.util.concurrent.atomic.AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
/*
* This class intended to be implemented using VarHandles, but there
* are unresolved cyclic startup dependencies.
*/
private static final Unsafe U = Unsafe.getUnsafe(); // 官方推荐用户代码用 VarHandle, 它自己却用 Unsafe: VarHandle 初始化本身就依赖原子操作, 启动期有循环依赖
private static final long VALUE
= U.objectFieldOffset(AtomicInteger.class, "value"); // value 字段相对对象基址的偏移量, 类加载时算一次。CAS 是内存地址级操作, 靠"基址+偏移"定位那 4 字节
private volatile int value; // volatile 管可见性, CAS 管原子性, 缺一个都不线程安全
public AtomicInteger(int initialValue) {
value = initialValue; // 构造期无竞争, 普通写即可
}
// ...
public final int get() {
return value; // 普通读的正确性全靠 value 的 volatile 修饰, 不经过 Unsafe
}
public final void set(int newValue) {
value = newValue; // 单纯覆盖不需要 CAS: 没有"依赖旧值"就没有竞态
}
public final void lazySet(int newValue) {
U.putIntRelease(this, VALUE, newValue); // release 语义: 省掉最贵的 StoreLoad 屏障, 新值稍后才对别的线程可见, 适合"清空引用"这类不急的写
}
// ...
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue); // CAS 核心入口: "当前值==期望值才写入"整体是一条 CPU 指令, 不可打断
}
// ...
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1); // i++ 语义, 返回旧值
}
// ...
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1; // ++i 语义。硬件只提供"返回旧值"的 fetch-and-add, 新值在本地 +1 算出, 绝不能再读一次内存(可能已被别人改)
}
// ...
public final int addAndGet(int delta) {
return U.getAndAddInt(this, VALUE, delta) + delta;
}
// ...
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev = get(), next = 0;
for (boolean haveNext = false;;) {
if (!haveNext)
next = updateFunction.applyAsInt(prev); // 竞争下函数会被反复执行, 所以要求它无副作用
if (weakCompareAndSetVolatile(prev, next))
return next;
haveNext = (prev == (prev = get())); // 重读后值没变 => 刚才是 weak CAS 的"伪失败", next 仍有效, 下一轮跳过重算
}
}
// ...
public final int compareAndExchange(int expectedValue, int newValue) {
return U.compareAndExchangeInt(this, VALUE, expectedValue, newValue); // 返回"见证值"(内存里实际的值): CAS 失败时不用再 get() 一次就知道输给了谁
}
// ...
}所有加法最终落到 Unsafe.getAndAddInt,一段 CAS 自旋;再往下两层就到 native 指令:
代码块收起展开
// 基于 JDK 17 本地源码, jdk.internal.misc.Unsafe
public final class Unsafe {
// ...
@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 每轮重读最新值; CAS 失败说明这一瞬间有人抢先改了, 拿旧值重试必然再败
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v; // 返回自增前的旧值
}
// ...
@IntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x); // Java 层兜底直接转调强 CAS; "weak 允许伪失败"的差别只在 JIT 内在化后的机器码里体现
}
// ...
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x); // 到底了: x86 上就是 lock cmpxchg 一条指令
// ...
}原理串讲
以一次 counter.incrementAndGet() 为例走完整链路。类加载时 objectFieldOffset 已把 value 字段的偏移量存进静态常量 VALUE——CAS 指令操作的是内存地址而非 Java 字段,所以每个方法都传 this + VALUE 这对组合,让 Unsafe 算出”对象基址 + 偏移”精确定位那 4 个字节。
这也是为什么偏移量只算一次存成 static final:它由类的字段布局决定,对所有实例都相同。
incrementAndGet() 调 U.getAndAddInt(this, VALUE, 1) + 1。
getAndAddInt 是标准 CAS 自旋:getIntVolatile 读出最新值 v,weakCompareAndSetInt 尝试”内存里还是 v 就写成 v + delta”;失败说明读和写之间有线程插队改了值,回到循环头重读重试,直到某一轮读-写之间无人打扰。
weakCompareAndSetInt 在 Java 层直接转调 native 的 compareAndSetInt,最终对应 x86 的 lock cmpxchg。
但生产环境里这个循环大多根本不执行——@IntrinsicCandidate 标记让 JIT 把整个 getAndAddInt 替换成一条 lock xadd(fetch-and-add)指令,无循环、无失败、必定成功。
为什么源码还要写 Java 循环?它是不支持该指令平台的语义兜底:正确性由这段代码保证,性能交给内在化。
这也解释了为什么循环里用 weak 版本 CAS:weak 允许 ARM 这类 LL/SC 架构上的”伪失败”(值没变也可能失败),换来更便宜的指令;反正外面套着循环,伪失败无非多转一圈,无害。
为什么 getAndAddInt 返回旧值、incrementAndGet 自己 + 1?硬件 fetch-and-add 天然返回旧值,而新值 = 旧值 + delta 在本地就能算出——绝不能为了拿新值再读一次内存,那一瞬间的值可能已经被别的线程改掉,返回它就破坏了原子语义。
同理 getAndIncrement(i++ 语义)直接返回旧值,两个方法共享同一条底层指令,差别只在 Java 层的一次加法。
最后,value 的 volatile 不是可有可无的:CAS 只保证”改”这一步原子,get() 这种普通读的可见性、以及自旋里每轮能读到别人刚写入的新值,都靠 volatile 的内存屏障。去掉它,自旋线程可能永远读到缓存里的旧值,空转不止。
设计取舍
- CAS 是乐观策略:低竞争下一条用户态指令搞定,省掉 synchronized 可能的内核态切换;高竞争下自旋空转烧 CPU,写多读少的计数器换
LongAdder(分段累加、读时求和)。 - ABA 问题:
compareAndSet只看”当前值是否等于期望值”,A→B→A 会被误判为没变过。纯计数场景数值相等即语义相等,无所谓;栈顶、链表头这类指针场景要用带版本号的AtomicStampedReference。 - 原子性边界是”单变量、单操作”:
if (ai.get() == x) ai.set(y)不是原子的,两个 AtomicInteger 之间更没有原子性;复合逻辑用compareAndSet自旋或updateAndGet,跨变量只能上锁。 updateAndGet一族的函数参数必须无副作用:竞争下会被重复调用,副作用会被放大执行多次。compareAndExchange比compareAndSet多返回一个见证值,失败时省一次重读,是 JDK 9 后写自旋的更优选择。