引言
前六篇文章,我们构建了一个相对完整的并发世界观:从内存可见性的底层硬件原理,到锁的竞争与优化;从线程池的资源管理,到异步任务的流畅编排;从并发容器的线程安全,到多线程协作的同步屏障。现在我们把目光收束到一个最细小却最频繁的操作上——计数。
在系统中,计数无处不在:接口的每秒请求数(QPS)、在线用户数、订单累计金额、缓冲队列中的消息条数。对于这类操作,我们既不能容忍脏数据(计数不准),又不愿承受重量级锁带来的性能损耗。
Java早期提供的解决方案是AtomicInteger和AtomicLong,它们通过CAS(Compare-And-Swap)无锁算法实现了线程安全的原子递增。然而,当并发量攀升到一定层级时,这些原子类开始暴露出致命的性能瓶颈——高并发下的CAS自旋风暴。为解决这一痛点,Doug Lea在Java 8中引入了LongAdder,将计数性能推向了极致。本文将深度剖析原子类的底层实现原理,揭示高并发下CAS的困境,并逐层拆解LongAdder的分段累加设计哲学。
一、原子类的基石:Unsafe与CAS的轻量级护佑
原子类(AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等)是JUC中最轻量级的并发组件。它们既不使用synchronized的重量级互斥,也不涉及线程的挂起与唤醒,而是直接依赖sun.misc.Unsafe提供的硬件级CAS指令。
以AtomicInteger为例,其内部维护了一个volatile int value变量,确保了可见性。当调用incrementAndGet()时,底层进入一个自旋(死循环)模式:先读取当前值current,计算新值next,然后通过Unsafe.compareAndSwapInt原子性地将value从current更新为next。如果此时其他线程抢先修改了值,CAS返回false,循环继续重试,直到成功为止。
这套机制的美妙之处在于它无阻塞、无上下文切换。在低到中等冲突概率下,自旋重试的开销远小于线程挂起和唤醒的系统调用,因此原子类在绝大多数业务场景下表现得极为出色。对于单个变量的简单增减操作,它们是最优的默认选择。
二、高并发下的阿喀琉斯之踵:自旋开销与缓存伪共享
当大量线程在同一时刻对同一个AtomicLong执行incrementAndGet时,CAS操作会面临激烈的竞争。除了极少数线程能一次成功,其余线程必须进入循环不断重试,直到锁被释放或自己的时间片耗尽。这种CPU空转(Spin-Wait) 在高并发下会消耗大量的处理器时间,导致有效吞吐量骤降。
更深层的性能杀手在于缓存一致性的总线风暴。由于所有线程都在争抢同一块内存地址(即value属性),每次CAS尝试都会触发总线事务,强制各CPU核心缓存中对应的缓存行失效。根据MESI协议,这会导致大量的缓存行在“共享-失效”之间反复震荡,总线带宽被占满,系统整体性能急剧下降。当线程数超过CPU核心数时,AtomicLong的性能甚至可能劣于使用synchronized保护的朴素计数——因为自旋空转比让出CPU更浪费资源。
面对这种场景,我们直观的优化思路是:既然大家都争抢同一块内存会崩溃,为什么不给每个线程分配一块独立的内存区域用于计数,最后再汇总呢? 这正是LongAdder的核心设计起点。
三、LongAdder的空间换时间策略:热点分离
LongAdder是Striped64类的子类,其设计哲学可以用一句话概括:将单一热点的竞争压力分散到多个独立的计数单元(Cell)上。
在LongAdder内部,维护了两个核心字段:一个普通的base值和一个Cell数组。base类似于AtomicLong中的单一变量,在低并发场景下直接使用CAS操作base,行为与AtomicLong几乎无异。而Cell是一个带@Contended注解的填充类,每个Cell包装了一个volatile long value,并且通过填充字节独占一个缓存行,彻底避免了伪共享问题。
当高并发并发修改到来,且base上的CAS竞争失败时,LongAdder会采取分流策略。每个线程会被映射到Cell数组中的一个特定索引(通常基于线程的哈希值)。此时,线程不再去争夺base,而是转而对自己专属的Cell执行CAS递增。由于Cell数组的长度通常接近CPU核心数,映射到同一个Cell的线程数量远小于全局争夺的线程数,CAS冲突率急剧下降,CPU空转消耗大幅减少。
四、精准的线程定位与哈希冲突处理
在LongAdder的add(long x)方法中,逻辑分支处理得极为细腻。它会先尝试在base上执行CAS累加,若base竞争失败或当前处于扩容状态,则通过当前线程的probe哈希值定位到Cell数组的某个槽位。若该槽位为空,则会尝试初始化一个新的Cell对象并安置到数组中。
如果映射到的Cell上也发生了CAS冲突,LongAdder不会无限自旋,而是通过collide标志位决定是否需要扩容Cell数组或重新哈希当前线程的probe值,让线程映射到另一个Cell上。这种退避机制巧妙地利用了“线程随机重定向”来分散压力,使得即使在高并发极端压力下,Cell数组也能动态调整到足够大的容量来匹配当前的并发度,直至最大容量达到CPU核心数为止。
五、最终求和的弱一致性取舍
LongAdder的sum()方法会累加base和所有Cell中的value,并返回总和。但值得注意的是,sum()在执行过程中并不加锁,也不进行快照冻结。这意味着如果此时恰好有线程在修改某些Cell的值,sum()返回的是一个弱一致性(最终一致性) 的近似值,而非某一时刻的绝对精确快照。
这一取舍完全合理。在高频计数的场景下(如统计秒级流量),我们往往更关注吞吐量和最终走势,而允许中间状态的极小偏差。如果你要求绝对的精确快照(例如扣减库存的原子增减),AtomicLong或加锁保护才是正确的选型。
六、关于伪共享的纵深防御:@Contended注解的作用
在Striped64的内部类Cell上,我们可以看到sun.misc.Contended注解。这个注解在Java 8中通过填充128字节的前后补丁,强制将Cell对象占满整个缓存行(64字节或128字节)。
为什么要做这件事?在Cell数组中,相邻的Cell对象在物理内存上紧挨着。当线程A修改Cell[0]时,该缓存行被标记为Modified,导致线程B所在核心的缓存行失效。而线程B恰好要操作相邻的Cell[1],这两个毫不相干的数据却被强行绑定在同一个缓存行中,导致线程B不得不重新加载,引发巨大的性能损耗。通过@Contended填充,每个Cell独自占用完整的缓存行,彻底消除了这种“物理邻近性”带来的虚假依赖,使得多核心并行修改不同Cell时能够互不干扰。
七、生产环境选型决策:AtomicLong vs LongAdder
在实战中如何选型,有一条清晰的分界线可以遵循。
当竞争程度较低,或对计数值的实时精确性要求极高时,优先选用AtomicLong。例如在生成全局唯一ID的种子、或精确扣减余额等场景中,每一次递增都必须被立即无误地确认,此时AtomicLong直接了当。
当并发量极高(线程数远超核心数)、允许瞬时计数存在微小误差、且主要目的是统计汇总值时,LongAdder是碾压级的胜出者。典型的应用场景包括:Web容器的请求计数器、Hystrix的滚动窗口统计、Log4j2的异步日志计数等。在这些场景中,sum()方法的短暂不精确性完全不影响宏观监控的准确度,而吞吐量的成倍提升所带来的收益远大于那一点可忽略不计的偏差。
八、拓展视野:AtomicReference与AtomicIntegerFieldUpdater
除了基础的数值原子类,AtomicReference允许我们以原子方式更新对象引用,是实现无锁栈(Lock-Free Stack)和无锁队列(如ConcurrentLinkedQueue)的基础组件。其CAS语义与数值类完全一致,只不过比较的是对象的引用地址(内存指针)。
AtomicIntegerFieldUpdater则是一种更为极客的工具。它允许将普通volatile int字段升级为原子操作对象,而不需要将整个类封装成原子包装类型。这在需要节约内存的极大规模对象数组场景下尤为实用——因为每个AtomicInteger是一个独立的对象,存在头指针等额外对象开销,而FieldUpdater操作的是原始字段,内存占用不变。但使用时必须格外谨慎,字段必须是volatile修饰且不能是static,否则会抛出异常。
结语
原子类与LongAdder的演进,清晰地折射出并发框架设计中的永恒博弈——在精确性、性能和内存占用之间寻找最优解。AtomicLong以简单直接的CAS策略解决了基础原子计数需求,而LongAdder则敏锐地洞察到真实场景中的“计数无需绝对实时”这一宽松约束,通过空间换时间和热点分离的艺术,将多核CPU的潜力压榨到了极致。
当你下一次面对高并发流量统计时,不妨深思:你的业务是需要每一次增减的绝对权威答复,还是只需要最终能够大致对齐的流量画像?理解了这个哲学,你手中的工具选择便永远不会出错。

