给计数器加一,除了使用 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 问题。

最后修改:2018 年 02 月 11 日
如果觉得我的文章对你有用,请随意赞赏