【JUC】原子类

深入浅出 Java 多线程原子类

一、为什么需要原子类?

先看一个经典问题:

java 复制代码
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // 这行代码不是原子的!
    }
}

count++ 看起来是一行,实际上分为三步:

  1. 读取 count 的值
  2. 加 1
  3. 写回 count

多线程同时执行时,可能两个线程都读到 count=5,都加 1 写回 6,结果本应是 7 却变成了 6------这就是竞态条件。

传统解决方案是加锁(synchronized),但锁太重了。原子类提供了无锁的解决方案。


二、原子类的核心原理:CAS

原子类的基石是 CAS(Compare-And-Swap,比较并交换):

复制代码
CAS(内存地址, 期望值, 新值):
    if (内存地址的值 == 期望值) {
        内存地址的值 = 新值;
        return true;
    }
    return false;

关键点:这一整步操作由 CPU 指令保证原子性 (如 x86 的 cmpxchg 指令),不需要加锁。

用 CAS 重写 count++:

复制代码
do {
    old = count;           // 读取当前值
    new = old + 1;         // 计算新值
} while (!CAS(count, old, new));  // 如果 count 还是 old,就更新;否则重试

这就是乐观锁思想:假设没有冲突,冲突了就重试。


三、原子类家族

java.util.concurrent.atomic 包下主要有四类:

1. 基本类型原子类
  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
2. 引用类型原子类
  • AtomicReference<V> ------ 原子更新对象引用
  • AtomicStampedReference<V> ------ 带版本号,解决 ABA 问题
  • AtomicMarkableReference<V> ------ 带布尔标记
3. 数组类型原子类
  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray<E>

注意 :原子数组只保证数组元素的原子性,不保证整个数组。

4. 字段更新器(反射方式更新对象字段)
  • AtomicIntegerFieldUpdater<T>
  • AtomicLongFieldUpdater<T>
  • AtomicReferenceFieldUpdater<T,V>
java 复制代码
class User {
    volatile int age;  // 字段必须是 volatile
}

AtomicIntegerFieldUpdater<User> updater =
    AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
updater.incrementAndGet(user);

JDK 8 还加入了 LongAdder、DoubleAdder(高并发场景下性能更好,后文详述)。


四、常用 API(以 AtomicInteger 为例)

java 复制代码
AtomicInteger ai = new AtomicInteger(0);

ai.get();                        // 获取当前值
ai.set(10);                      // 设置值
ai.getAndSet(20);                // 获取旧值并设置为新值

ai.incrementAndGet();            // 先加1再返回(++i)
ai.getAndIncrement();            // 先返回再加1(i++)
ai.decrementAndGet();            // --i
ai.getAndDecrement();            // i--

ai.addAndGet(5);                 // 加5并返回新值
ai.getAndAdd(5);                 // 返回旧值再加5

ai.compareAndSet(expect, update); // CAS 操作,成功返回 true
ai.weakCompareAndSet(expect, update); // 可能虚假失败,但性能稍好

五、ABA 问题

CAS 有一个经典陷阱:ABA 问题。

场景:

  1. 线程1 读到值 A,准备改成 C
  2. 线程2 把 A 改成 B,又改回 A
  3. 线程1 的 CAS 检查发现还是 A,成功更新为 C

但中间其实已经变过了!某些场景(如栈、链表)这会导致数据错误。

解决方案 :AtomicStampedReference,用"版本号"配合值一起比较。

java 复制代码
AtomicStampedReference<Integer> ref = 
    new AtomicStampedReference<>(100, 0);  // 值100,版本0

int stamp = ref.getStamp();
Integer value = ref.getReference();

// 只有值和版本号都匹配才更新
ref.compareAndSet(value, 200, stamp, stamp + 1);

六、性能对比与 LongAdder

普通场景下,原子类性能优于 synchronized。但高并发下,多个线程疯狂 CAS 重试,CPU 会空转。

JDK 8 引入了 LongAdder,思路是分段累加:

复制代码
LongAdder 内部维护一个 base + Cell[] 数组
- 无竞争时直接 CAS base
- 有竞争时,不同线程分散到不同 Cell 上累加
- 求总和时把 base + 所有 Cell 相加
场景 推荐
低并发、需要精确 get 值 AtomicInteger / AtomicLong
高并发累加统计 LongAdder
需要 CAS 逻辑 AtomicXXX
java 复制代码
LongAdder adder = new LongAdder();
adder.increment();      // 各线程分散累加
adder.sum();            // 汇总(非原子快照)

七、实战示例

场景1:线程安全计数器

java 复制代码
AtomicInteger counter = new AtomicInteger();
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    pool.submit(counter::incrementAndGet);
}
pool.shutdown();
// counter.get() == 1000

场景2:无锁单例(CAS 实现)

java 复制代码
class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = 
        new AtomicReference<>();
    
    public static Singleton getInstance() {
        for (;;) {
            Singleton inst = INSTANCE.get();
            if (inst != null) return inst;
            inst = new Singleton();
            if (INSTANCE.compareAndSet(null, inst)) return inst;
        }
    }
}

场景3:状态机的原子切换

java 复制代码
AtomicReference<State> state = new AtomicReference<>(State.INIT);
// 只有当前是 INIT 才转 RUNNING
state.compareAndSet(State.INIT, State.RUNNING);

八、注意事项

  1. 原子类只保证单个变量的原子性。多个变量组合操作仍需加锁。
  2. 复合操作不原子 :if (ai.get() > 0) ai.decrementAndGet(); 不是原子的,要配合 CAS 循环。
  3. ABA 问题在链式结构、栈操作中要小心。
  4. 原子引用不保证对象内部字段的原子性,只是引用的赋值原子。
  5. 字段更新器的字段必须 volatile,且不能是 private(反射访问需要)。
  6. 高并发累加 优先考虑 LongAdder。

九、总结一句话

原子类 = volatile 保证可见性 + CAS 保证原子性 + 乐观重试代替阻塞加锁,是 Java 并发编程中最轻量的无锁利器。

理解了 CAS,就理解了原子类的灵魂;理解了 ABA 和 LongAdder,就摸到了它的边界和升级方向。

相关推荐
惊讶的猫1 小时前
Query 改写
java·rag
信誓旦旦的程序猿1 小时前
【Python量化实战宝典 #18】MACD 金叉死叉手册:DIF、DEA、柱体全拆解(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
Misnearch2 小时前
第7章 面向对象技术
java·开发语言·软考
weixin_BYSJ19872 小时前
电竞先锋网站---附源码02032
java·vue.js·spring boot·python·django·tomcat·php
会编程的吕洞宾2 小时前
LangChain4j 流式输出实战:让 AI 回复像打字机一样逐字显示
java·开发语言·人工智能·后端
只是学习学习2 小时前
Java将文件转二维码导出MP4
java
曹牧2 小时前
JDK重定向目录
java·开发语言
郑州光合科技余经理2 小时前
同城小程序开发:同一笔单在用户端和商家端各显示到哪
android·java·开发语言·前端·后端·uni-app·php
code2cat3 小时前
【随笔】RAG检索缓存:复用证据之前,先把权限和版本放进缓存键
java·人工智能·缓存·知识库·rag