给计数器加一,除了使用 synchronized,还可以用 AtomicInteger.incrementAndGet()。这个方法如何避免两个线程把同一次更新覆盖掉?先看 CAS。
比较后更新
CAS 是 Compare And Swap,包含预期值和新值。只有当前值仍然等于预期值,才把它替换成新值;比较和替换作为一个原子操作完成。
import java.util.concurrent.atomic.AtomicInteger;
public class CasDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger count = new AtomicInteger();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
int oldValue;
do {
oldValue = count.get();
} while (!count.compareAndSet(oldValue, oldValue + 1));
}
};
Thread a = new Thread(task);
Thread b = new Thread(task);
a.start();
b.start();
a.join();
b.join();
System.out.println(count.get());
}
}结果是 2000。假如两个线程都读取到 10,一个成功改为 11,另一个的 CAS 就会失败。失败的一方重新读取当前值,再计算和尝试。
实际写计数器直接使用 incrementAndGet() 即可,这里展开循环只是为了看清过程。
AtomicInteger 和 Unsafe
在 JDK 8 的 AtomicInteger 中,数值保存在 volatile 字段里,CAS 操作会调用 Unsafe.compareAndSwapInt(),传入对象、字段偏移量、预期值和新值。
volatile 负责相应的内存可见性与顺序,CAS 负责有条件更新的原子性。底层最终需要 JVM 和处理器提供支持,不能用普通 Java 的 if (value == expected) value = update 来代替。
Unsafe 能执行字段偏移访问、CAS 等底层操作,但它不是普通业务代码应该直接依赖的标准公共 API。原子类已经把这里需要的能力封装好了。
重试有代价
CAS 没有在这段循环里挂起线程,但失败仍然要重试。同一个位置竞争越激烈,反复读取和更新的成本可能越高。不能简单说“没有 synchronized,所以一定更快”。
循环内的计算还必须允许重复执行。比如在 CAS 成功前先发消息、扣其他账户余额,失败重试时就可能重复产生副作用。updateAndGet() 接收的更新函数也有同样要求。
CAS 通常只处理一个值的条件更新。两个字段之间有约束时,可以把它们封装成不可变对象,通过 AtomicReference 替换整个对象,也可以直接用锁保护一组操作。
还有一种情况:数值从 A 改成 B,再改回 A,CAS 只比较当前值,看不出中间发生过变化。这就是后面要说的 ABA 问题。