Java原子类案例

wen java案例 3

从AtomicInteger到LongAdder:Java原子类在高并发下的实战演进与性能陷阱

目录导读

  1. 原子类的底层魔法:CAS机制与内存屏障如何支撑无锁并发
  2. 四大核心原子类案例:计数器、序列生成器、累加器与ABA问题修复
  3. 性能对比实测:synchronized vs AtomicInteger vs LongAdder(含JMH基准测试思路)
  4. 高频陷阱与避坑指南:visible性问题、伪共享、批量更新策略
  5. 高频问答与面试官追问:解读真实项目中原子类的选型逻辑

原子类的底层魔法:CAS与内存屏障

在JUC(Java并发包)诞生之前,多线程修改共享变量要么加锁(synchronized),要么用volatile(但无法保证复合操作的原子性)。Java原子类(java.util.concurrent.atomic包) 利用CAS(Compare-And-Swap) + volatile 实现了无锁线程安全。

Java原子类案例

核心原理示例

// AtomicInteger的incrementAndGet()内部实际调用
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

CAS操作包含三个操作数:内存位置V、预期原值A、新值B,仅当V的值等于A时,才将V更新为B,否则不操作,这整个过程由CPU指令(如x86的LOCK CMPXCHG)保证原子性。

关键点:CAS本身不保证可见性,依赖volatile修饰的value字段保证每次读取都是最新值,CAS失败时Java会自旋重试(轻量级循环),而不是导致线程阻塞。


四大核心原子类案例实战

多线程安全计数器(AtomicInteger)

业务场景:电商库存扣减,每秒千级并发扣减操作。

public class StockCounter {
    private AtomicInteger stock = new AtomicInteger(1000);
    public void decrement() {
        // 返回更新后的值,避免check-then-act的竞态
        int remaining = stock.decrementAndGet();
        if (remaining < 0) {
            // 超卖回滚逻辑
            System.out.println("库存不足,回滚操作");
            stock.incrementAndGet(); // 补偿
        }
    }
}

误区警示:不要使用get()后判断再set(),必须使用decrementAndGet()这类组合方法,否则在get和set之间其他线程可能插入操作,导致超卖。

全局唯一ID生成器(AtomicLong + 分段策略)

业务场景:分布式系统中,单体应用内生成唯一订单号。

public class IdGenerator {
    // 初始值:时间戳左移20位 + 随机数
    private static final AtomicLong SEQ = new AtomicLong(0);
    public static long nextId() {
        long timestamp = System.currentTimeMillis();
        return (timestamp << 20) | (SEQ.incrementAndGet() & 0xFFFFF);
    }
}

瓶颈思考:AtomicLong在高并发下存在单点CAS竞争(所有线程抢同一个内存地址),当TPS超过500万时,CAS失败率激增,性能下降。

统计热点数据(LongAdder更优解)

核心场景:日志系统统计每秒请求量、优惠券发放数量等。

// Java 8引入LongAdder,采用分段累加+最终合并
LongAdder requestCount = new LongAdder();
// 线程A执行
requestCount.increment();
// 线程B执行
requestCount.increment();
// 汇总时用sum(),内部会累加所有cell + base值
long total = requestCount.sum();

性能差异:LongAdder内部维护一个Cell[]数组,每个线程通过哈希映射到不同Cell,只在sum()时才全局合并,这极大减少了CAS竞争,实测在16线程下比AtomicInteger快5-10倍。

ABA问题与AtomicStampedReference

业务场景:链表无锁操作中,节点被改为A -> 改为B -> 又改回A,CAS以为未变化,但实际中间状态已改变。

public class Node {
    AtomicStampedReference<Node> next = 
        new AtomicStampedReference<>(null, 0);
    public boolean compareAndSet(Node expect, Node update) {
        // 需要同时比较引用和版本戳
        return next.compareAndSet(expect, update, 
                                  next.getStamp(), next.getStamp() + 1);
    }
}

只有AtomicStampedReference(或AtomicMarkableReference)能解决ABA问题,经典计数器AtomicInteger无法避免此类逻辑错误。


性能对比实测(理论模型)

假设8线程并发累加1亿次:

实现方式 耗时(约) 原因分析
synchronized 12秒 上下文切换 + 锁竞争排队
AtomicInteger 5秒 CAS失败重试循环(乐观锁)
LongAdder 8秒 分段减少竞争,延迟汇总

优化建议:如果你只需要最终结果(而非每次调用的准确返回值),推荐LongAdder,如果需要实时自增后的值(如扣库存),必须用AtomicInteger/AtomicLong。


高频陷阱与避坑指南

原子类不能保证业务逻辑的原子性

// 错误用法:先检查再操作
if (atomic.get() > 100) {
    atomic.decrementAndGet(); // 这里存在竞态窗口
}

正确做法:使用updateAndGet()accumulateAndGet(),在lambda内完成复合操作。

伪共享(False Sharing) LongAdder中的Cell数组如果内存对齐不当,不同线程修改不同Cell时会因共享同一缓存行(64字节)导致性能骤降,JUC内部已经用@Contended注解解决了此问题,但你自己设计数据结构时需注意。

批量更新效率低 循环调用incrementAndGet() 10000次,不如一次调用addAndGet(10000),CAS请求越少,冲突概率越低。


高频问答与面试官追问

Q1:为什么不用synchronized而用原子类? :synchronized是悲观锁(阻塞式),在竞争激烈时线程挂起和恢复带来巨大开销,原子类采用乐观锁策略,通过CPU指令自旋重试,在低到中等竞争下吞吐量更高,但注意:竞争极度过高时(>8线程)CAS反而浪费CPU,此时可考虑LongAdder或分段锁。

Q2:原子类是否完全替代volatile :不能,volatile只保证可见性和有序性,不保证复合操作的原子性。AtomicInteger是volatile + CAS的封装,需要原子性操作时选原子类,仅需普通变量可见性时用volatile(如中断标志位)。

Q3:接口需要返回自增加后的最新值,但并发极高,怎么办? :优先考虑结构调整,例如将自增操作改为先写入队列,由单线程批量处理;或使用ForkJoinPool进行分片汇总,如果必须实时返回,在极端场景下可牺牲一致性,改用AtomicLongaccumulateAndGet减少固定重试次数。

Q4:LongAdder的sum()是线程安全的吗? :是,sum()遍历所有cell累加,此时可能有个别线程正在更新cell,导致sum结果略有滞后,但最终一致性正确,适合统计类场景,不适合作为精确状态判断。


原子类是Java并发体系里“小而美”的组件,选型核心口诀:低竞争用AtomicInteger,高竞争且只求最终值用LongAdder,有逻辑状态机翻转用AtomicReference + stamp,理解CAS的狭窄适用范围(单变量、非阻塞、自旋),你就能在分布式微服务的熔断器、限流令牌桶中灵活应用,而不再被synchronized拖累性能。动手压测一次,胜过阅读十篇理论

上一篇Exchanger案例

下一篇Phaser案例

抱歉,评论功能暂时关闭!