LongAdder

一、引言:AtomicLong 的"自旋"困境

LongAdder 出现之前,Java 高并发计数的标准答案是 AtomicLong。它通过 CAS(Compare-And-Swap) 实现了线程安全的原子性累加,在低并发场景下表现完美。

但 CAS 有一个致命的弱点------自旋 重试 。当竞争激烈时,大量线程同时 CAS 修改同一个 value,只有一个能成功,其余线程会反复重试,陷入不停的自旋中。线程越多,竞争越激烈,CAS 失败率越高,CPU 空转越严重,性能急剧下降。

LongAdder 的核心思想分散热点 。将一个共享的 value 拆分成多个独立的"计数单元"(Cell),每个线程只操作自己分配到的那个单元,将竞争压力从"一点"分散到"多点"。

这个思想可以类比为超市收银AtomicLong 就像只有一个收银台,所有顾客排一条长队;LongAdder 则是开了多个收银台,顾客分散到不同柜台,大大减少排队等待。

二、核心设计思想:分段累加 + 分散竞争

LongAdder 的设计可以用一句话概括:无竞争时用 base ,有竞争时用 Cell[] 数组分摊压力

2.1 数据结构

LongAdder 继承自 Striped64 抽象类,内部维护三个核心变量:

java 复制代码
// 位于 Striped64 中
transient volatile Cell[] cells;   // 计数单元数组,每个 Cell 独立计数
transient volatile long base;      // 基础值,无竞争时使用
transient volatile int cellsBusy;  // 自旋锁,0表示空闲,1表示锁定(用于创建/扩容 cells)

2.2 Cell 内部类:避免伪共享

CellStriped64 的内部类,每个 Cell 就是一个独立的计数器:

java 复制代码
/**
 * Cell 是 LongAdder 和 ConcurrentHashMap 中用于分段计数的核心单元。
 * 每个 Cell 相当于一个独立的计数器,用于分摊并发更新压力。
 * 
 * 类比:每个 Cell 是一个收款柜台,每个柜台有自己的账本,各自算账,最后汇总。
 */
@sun.misc.Contended  // 关键注解:消除伪共享!
static final class Cell {
    /**
     * volatile 保证多线程间的可见性:
     * 一个线程修改了 value,其他线程立即能看到最新值
     */
    volatile long value;
    
    Cell(long x) { 
        this.value = x; 
    }
    
    /**
     * CAS 原子更新:尝试把 value 从 cmp 更新为 val
     * @param cmp 期望值(我期望 value 现在等于多少)
     * @param val 新值(如果满足期望,我就改成这个)
     * @return true 更新成功,false 说明被其他线程抢先修改了
     * 
     * 类比:多个顾客同时给同一个柜台交钱,只有第一个成功,
     * 后面的要重试(重新读取当前值再次尝试)。
     */
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

@Contended 注解是性能的关键 。CPU 缓存以 64 字节的缓存行(Cache Line) 为单位加载数据。如果多个 Cell 位于同一缓存行,一个 Cell 的更新会导致整行失效,其他 Cell 的读取也要重新从内存加载------这就是伪共享(False Sharing)@Contended 让每个 Cell 独占缓存行,避免了这种性能损耗。

2.2.1什么是伪共享(False Sharing)?

先理解 CPU 缓存行(Cache Line):

CPU 读内存时,不是"要一个字节就读一个字节",而是"整块整块搬"。通常一块是 64 字节------这就是缓存行。比如你想读一个小数(8 字节),CPU 会一次性把这块 64 字节区域内的所有数据都拉到缓存里,因为 CPU 觉得你接下来很可能也会用到邻近的数据。

使用**@Contended** 是在强制每一个cell独占一个缓存行,避免多个cell挤在一起互相影响。

2.3 工作流程

LongAdderadd() 方法遵循三级策略

|---------|------------------------------------|-----------------------------------------|
| 步骤 | 条件 | 操作 |
| 第一级 | cells == null 且 CAS 更新 base 成功 | 直接操作 base,与 AtomicLong 无异 |
| 第二级 | cells 已存在,线程找到对应 Cell 且 CAS 成功 | 更新该 Cell 的值 |
| 第三级 | 上述均失败(Cell 不存在或 CAS 失败) | 调用 longAccumulate():创建/扩容 cells 或重试 |

三、源码深度剖析

3.1 add() 方法:入口与三级策略

java 复制代码
/**
 * LongAdder 的核心方法:累加一个值
 * 
 * 设计思想:先抢 base,抢不到就找自己的 Cell 加,Cell 也失败就进入复杂处理
 * 
 * 类比:银行有 1 个普通柜台(base)和 N 个 VIP 窗口(cells),
 *       每个顾客(线程)先尝试普通柜台,如果忙就去自己的专属窗口。
 */
public void add(long x) {
    Cell[] cs;      // cells 数组:多个分段计数器(VIP 窗口列表)
    long b, v;      // b: base 当前值,v: Cell 的当前值
    int m;          // cells 数组长度 - 1(用于位运算取模)
    Cell c;         // 当前线程对应的 Cell
    
    /**
     * ===== 第一级:尝试直接更新 base(普通柜台) =====
     * 
     * 条件 1:(cs = cells) != null
     *   - 如果 cells 已经存在,说明之前发生过竞争,直接跳过 base,走分段逻辑
     * 
     * 条件 2:!casBase(b = base, b + x)
     *   - 如果 cells == null,尝试 CAS 更新 base(普通柜台)
     *   - 成功 → 加完了,直接返回
     *   - 失败 → 说明有竞争,进入下一级
     * 
     * 这里的 || 是短路的:只要 cells != null,casBase 根本不会执行
     */
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        // 进入这里说明:cells 已存在 或 更新 base 失败(出现竞争)
        boolean uncontended = true;  // 标记:是否没有竞争
        
        /**
         * ===== 第二级:尝试更新对应的 Cell(专属窗口) =====
         * 
         * 条件 1:cs == null
         *   - cells 数组不存在(可能是初始化失败)
         * 
         * 条件 2:(m = cs.length - 1) < 0
         *   - 数组长度为 0(边界情况)
         * 
         * 条件 3:(c = cs[getProbe() & m]) == null
         *   - 当前线程探针值对应的 Cell 还没创建
         *   - getProbe():当前线程的哈希值(类似排队号)
         *   - getProbe() & m:用位运算取模,定位到数组中的某个位置
         *   - 如果这个位置是 null,说明还没给这个线程分配专属窗口
         * 
         * 条件 4:!(uncontended = c.cas(v = c.value, v + x))
         *   - CAS 更新 Cell 失败(有其他线程也在改这个 Cell)
         *   - uncontended = false 表示发生了竞争
         */
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x))) {
            
            /**
             * ===== 第三级:进入复杂处理 =====
             * 
             * 触发条件(四种情况任意一种):
             * 1. cs == null:cells 数组不存在 → 需要初始化
             * 2. 数组长度为 0:边界异常 → 需要修复
             * 3. 当前线程的 Cell 为 null:没有专属窗口 → 需要创建
             * 4. CAS 更新 Cell 失败:发生竞争 → 需要扩容或重新定位
             * 
             * longAccumulate 负责:
             * - 初始化 cells 数组
             * - 创建当前线程的 Cell
             * - 扩容 cells 数组(减少竞争)
             * - 重置当前线程的探针值(重新定位)
             */
            longAccumulate(x, null, uncontended);
        }
    }
}

关键点解读

  1. cells != null :只要 cells 数组已存在,说明之前发生过竞争,直接走分段逻辑

  2. !casBase(...) :如果 cells == null,尝试 CAS 更新 base;成功则结束,失败说明出现竞争

  3. getProbe() :获取当前线程的探针值(Probe) ------ 一个由 ThreadLocalRandom 生成的哈希值,用于定位 Cell 数组的槽位

  4. cs[getProbe() & m] :用探针值对数组长度取模(位运算),找到当前线程对应的 Cell

|-----------------------|-------------------------|------------------------------------------------|
| 关键点 | 技术含义 | 通俗理解 |
| cells != null | 只要 cells 数组存在,说明之前发生过竞争 | 银行只要开过 VIP 窗口,就说明以前人多过,新顾客就别去普通柜台排队了,直接去 VIP 区 |
| !casBase(...) | CAS 更新 base 失败,说明出现竞争 | 普通柜台有人,你抢不到,只能去 VIP 区 |
| getProbe() | 每个线程的哈希值 | 你的排队号,决定你去哪个 VIP 窗口 |
| csgetProbe() \& m | 用探针值对数组长度取模定位 Cell | 排队号对窗口数取余,决定你去几号窗口 |

3.2 longAccumulate():Cell 的创建、扩容与重试

longAccumulate()Striped64 中的核心方法,处理 Cell 不存在、CAS 失败等复杂情况。

java 复制代码
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    // 初始化线程探针(如果为0)
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current();  // 强制初始化
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;  // 是否发生碰撞(需要扩容的标志)
    
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        if ((as = cells) != null && (n = as.length) > 0) {
            // === Case 1: cells 已存在 ===
            if ((a = as[(n - 1) & h]) == null) {
                // 当前线程对应的 Cell 为空 → 创建新 Cell
                if (cellsBusy == 0) {  // 锁空闲
                    Cell r = new Cell(x);
                    if (cellsBusy == 0 && casCellsBusy()) {  // 尝试获取锁
                        // 将新 Cell 放入数组
                        if (as[(n - 1) & h] == null) {
                            as[(n - 1) & h] = r;
                            break;
                        }
                        cellsBusy = 0;  // 释放锁
                    }
                }
                collide = false;
            } else if (!wasUncontended) {
                // CAS 已知失败,重置标志,下次循环重试
                wasUncontended = true;
            } else if (a.cas(v = a.value, v + x)) {
                // 尝试 CAS 更新 Cell,成功则退出
                break;
            } else if (n >= NCPU || cells != as) {
                // 数组长度已达到 CPU 核数,或数组已被其他线程扩容,不再扩容
                collide = false;
            } else if (!collide) {
                // 发生碰撞,设置标志,下次循环考虑扩容
                collide = true;
            } else if (cellsBusy == 0 && casCellsBusy()) {
                // 获取锁,进行扩容(容量翻倍)
                if (cells == as) {
                    Cell[] rs = new Cell[n << 1];
                    System.arraycopy(as, 0, rs, 0, n);
                    cells = rs;
                }
                cellsBusy = 0;
                collide = false;
                continue;
            }
            // 更新探针值,尝试新的槽位
            h = advanceProbe(h);
        } else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            // === Case 2: cells 未初始化 → 初始化 ===
            boolean init = false;
            try {
                if (cells == as) {
                    Cell[] rs = new Cell[2];  // 初始容量为 2
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init) break;
        } else {
            // === Case 3: 无法获取锁 → 回退到更新 base ===
            if (casBase(v = base, (fn == null) ? v + x : fn.applyAsLong(v, x)))
                break;
        }
    }
}

核心逻辑解读

  1. 初始化 :首次发生竞争时,创建长度为 2cells 数组

  2. 创建 Cell :当前线程的槽位为空时,在 cellsBusy 锁保护下创建新 Cell

  3. 扩容 :当多个线程竞争同一个 Cell 导致 CAS 失败时,会触发扩容,容量翻倍 ,最大不超过 CPU 核数

  4. 探针更新advanceProbe() 更新线程的探针值,让线程尝试映射到其他 Cell,减少哈希冲突

  5. 锁机制cellsBusy 是一个自旋锁 (0空闲,1锁定),用于保护 cells 数组的创建和扩容

3.3 sum() 方法:最终一致性

java 复制代码
public long sum() {
    Cell[] as = cells;
    long sum = base;
    if (as != null) {
        for (Cell a : as) {
            if (a != null) {
                sum += a.value;
            }
        }
    }
    return sum;
}

注意事项

  • sum()非原子性 的:遍历 Cell 数组时,可能有线程正在更新某些 Cell 的值

  • 因此 sum() 返回的是最终一致性的快照值,适合统计场景(如 QPS 统计),不适合需要精确中间值的场景

  • 如果需要精确值,应使用 AtomicLong 或加锁保护

3.4 reset() / sumThenReset():重置与读取

java 复制代码
public void reset() {
    Cell[] as = cells;
    base = 0L;
    if (as != null) {
        for (Cell a : as) {
            if (a != null) {
                a.value = 0L;
            }
        }
    }
}

public long sumThenReset() {
    Cell[] as = cells;
    long sum = base;
    base = 0L;
    if (as != null) {
        for (Cell a : as) {
            if (a != null) {
                sum += a.value;
                a.value = 0L;
            }
        }
    }
    return sum;
}

sumThenReset() 适用于周期性统计场景(如每分钟统计一次 QPS),读取后重置所有计数单元。

四、LongAdder vs AtomicLong:性能对决

|-----------|--------------------------|----------------------|
| 对比维度 | AtomicLong | LongAdder |
| 数据结构 | 单个 volatile long value | base + Cell[] 数组 |
| 竞争处理 | 所有线程 CAS 同一个变量 | 分散到不同 Cell,各自 CAS |
| 低并发性能 | 优秀(直接 CAS) | 接近(略有数组访问开销) |
| 高并发性能 | 急剧下降(大量自旋) | 稳定优异 |
| 内存占用 | 小 | 较大(以空间换时间) |
| 适用场景 | 需要精确中间值、低并发 | 统计计数、高并发 |

五、代码实战

5.1 基础用法:接口调用次数统计

java 复制代码
import java.util.concurrent.atomic.LongAdder;

public class ApiCallCounter {
    // 统计各接口的调用次数
    private final LongAdder totalCalls = new LongAdder();
    private final LongAdder successCalls = new LongAdder();
    private final LongAdder failureCalls = new LongAdder();
    
    public void recordCall(boolean success) {
        totalCalls.increment();  // 等价于 add(1)
        if (success) {
            successCalls.increment();
        } else {
            failureCalls.increment();
        }
    }
    public void printStats() {
        System.out.println("总调用: " + totalCalls.sum());
        System.out.println("成功: " + successCalls.sum());
        System.out.println("失败: " + failureCalls.sum());
        System.out.println("成功率: " + 
            (successCalls.sum() * 100.0 / totalCalls.sum()) + "%");
    }
}

5.2 高并发场景:QPS 统计

java 复制代码
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class QpsCounter {
    private final LongAdder counter = new LongAdder();
    private volatile long lastQps = 0;
    
    /**
     * 每次请求调用,增加计数
     */
    public void increment() {
        counter.increment();
    }
    
    /**
     * 每秒统计一次 QPS,然后重置
     */
    public void startReport() {
        Executors.newSingleThreadScheduledExecutor()
            .scheduleAtFixedRate(() -> {
                long qps = counter.sumThenReset();  // 读取并重置
                lastQps = qps;
                System.out.println("当前 QPS: " + qps);
            }, 1, 1, TimeUnit.SECONDS);
    }
    
    public long getLastQps() {
        return lastQps;
    }
}

5.3 配合 ConcurrentHashMap 使用

LongAdder 经常与 ConcurrentHashMap 配合,统计每个 key 的出现次数:

java 复制代码
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public class WordCount {
    private final ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
    
    /**
     * 统计某个词的出现次数
     */
    public void count(String word) {
        // computeIfAbsent 保证原子性创建 LongAdder
        map.computeIfAbsent(word, k -> new LongAdder()).increment();
    }
    
    public long getCount(String word) {
        LongAdder adder = map.get(word);
        return adder == null ? 0 : adder.sum();
    }
    
    public void printAll() {
        map.forEach((word, adder) -> 
            System.out.println(word + ": " + adder.sum()));
    }
}

六、最佳实践与避坑指南

✅ 推荐做法

  1. 统计场景优先选用:接口调用次数、QPS 统计、日志计数等

  2. 周期性读取用 sumThenReset():减少遍历开销

  3. 配合 ConcurrentHashMap 使用:实现高效的分布式计数器

  4. 不要求精确中间值时使用sum() 是最终一致性

七、总结

|----------|---------------------------------------------------|
| 维度 | 设计精髓 |
| 核心思想 | 分段累加 + 分散竞争,将单一热点拆分为多个独立计数单元 |
| 数据结构 | base(无竞争)+ Cell[](有竞争),@Contended 消除伪共享 |
| 竞争策略 | 无竞争用 CAS 更新 base → 有竞争分散到 Cell → CAS 失败则扩容/重试 |
| 性能优势 | 高并发下性能远超 AtomicLong,吞吐量提升数倍 |
| 适用场景 | 高并发统计计数(QPS、调用次数、事件计数),不追求精确中间值 |

一句话总结LongAdder 用"空间换时间"的哲学,将 AtomicLong 的"单点竞争"转化为"多点分摊",在高并发统计场景下实现了质的性能飞跃。它是 ConcurrentHashMap 分段锁思想在计数器领域的完美复刻。

相关推荐
月光船幽幽2 小时前
分层阈值规避归藏协议过度重置
人工智能·python
鱼子星_2 小时前
【C++】反向迭代器:反向迭代器的底层认识与模拟实现
开发语言·c++·笔记·stl
名字还没想好☜2 小时前
Java 用 MethodHandle 替代反射:调用性能实测、invokeExact 的坑与缓存
java·开发语言·缓存·反射·methodhandle
SMF19192 小时前
【Linux】完美解决缩略图工具gm调用java.io.FileNotFoundException: gm问题
java·开发语言·python
㳺三才人子2 小时前
初探 Data Analysis - Matplotlib
python·plotly·pandas·matplotlib
念何架构之路2 小时前
路由注册:RouterGroup(routergroup.go)
开发语言·后端·golang
Darkwanderor2 小时前
C++的流简介和简单使用
开发语言·c++
Python私教2 小时前
0、null、未采集:AI Agent 反馈系统最容易踩的语义坑
人工智能·python
北斗落凡尘3 小时前
LangGraph 入门实战(5)
python·langchain