性能远超AtomicLong,LongAdder原理完全解读

LongAdder是Java 8引入的一种高性能计数器,用于替代AtomicLong。它通过将计数压力分散到多个单元格,减少竞争并提高并发性能。本文详解LongAdder的工作原理及其实现细节。

高并发场景下可以选择 AtomicLong 原子类进行计数等操作,除了 AtomicLong,在 jdk1.8 中还提供了 LongAdder。PS:AtomicLong 在 jdk1.5 版本提供。

AtomicLong 底层使用 Unsafe 的 CAS 实现,当操作失败时,会以自旋的方式重试,直至操作成功。因此在高并发场景下,可能会有很多线程处于重试状态,徒增 CPU 的压力,造成不必要的开销。

LongAdder 提供了一个 base 值,当竞争小的情况下 CAS 更新该值,如果 CAS 操作失败,会初始化一个 cells 数组,每个线程都会通过取模的方式定位 cells 数组中的一个元素,这样就将操作单个 AtomicLong value 的压力分散到数组中的多个元素上。

通过将压力分散,LongAdder 可以提供比 AtomicLong 更好的性能。获取元素 value 值时,只要将 basecells 数组中的元素累加即可。

下面是它的原理实现。

    public void increment() {
        add(1L);
    }

    public void add(long x) {
        Cell[] as;
        long b, v;
        // m = as.length -1,取模用,定位数组槽
        int m;
        Cell a;
        // 低竞争条件下,cells 为 null,此时调用 casBase(底层为 CAS 操作,类似 AtomicLong) 方法更新 base
        // PS:cells 数组为懒加载,只有在 CAS 竞争失败的情况下才会初始化
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            // as 数组为 null,或者数组 size = 0,或者计算槽后在数组中不能定位,或者 cell 对象 CAS 操作失败
            if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

LongAdder 继承自 Striped64,底层调用 Striped64.longAccumulate 方法实现。

当第一次调用 add 方法时,并不会初始化 cells 数组,而是通过 CAS 去操作 base 值,操作成功后就直接返回了。

如果 CAS 操作失败,这时会调用 longAccumulate 方法,该方法会初始化 Cell 类型的数组,后面大部分线程都会直接操作这个数组,但是仍然有部分线程会更新 base 值。

Cell 元素定义如下:

    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

多个线程操作 cells 数组原理如下:

在这里插入图片描述
图片来源 LongAdder and LongAccumulator in Java

CPU 有多级缓存,这些缓存的最小单位是缓存行(Cache Line),通常情况下一个缓存行的大小是 64 字节(并不绝对,或者是 64 的倍数)。假设现在要操作一个 long 类型的数组,long 在 Java 中占 64 bit,8 个字节,当操作数组中的一个元素时,会从主存中将该元素的附近的其他元素一起加载进缓存行,即使其他元素你不想操作。

假设两个用 volatile 修饰的元素被加载进同一个缓存行,线程 A 更新变量 A 后会将更新后的值刷新回主存,此时缓存行失效,线程 B 再去操作 B 变量只能重新从主存中读取(Cache Miss)。这就造成了伪共享(False sharing)问题。

Cell 本身没什么好讲的,仔细看一下,这个类被 @sun.misc.Contended 注解修饰,这个注解一般在写业务时用不到,但是它可以解决上面的伪共享问题。

@sun.misc.Contended 注解在 jdk1.8 中提供,保证缓存行每次缓存一个变量,剩余的空间用字节来填充。

在这里插入图片描述
图片来源 LongAdder and LongAccumulator in Java

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        // 线程 threadLocalRandomProbe 属性值
        int h;
        if ((h = getProbe()) == 0) {
            // 初始化 Thread 的 threadLocalRandomProbe 属性值
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as;
            Cell a;
            // cells 数组大小
            int n;
            long v;
            // cells 数组已经初始化
            if ((as = cells) != null && (n = as.length) > 0) {
                // 当前线程对应数组槽的 Cell 对象为空
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        // 初始化 Cell,这里存在竞争,可能多个线程都创建了 Cell 对象
                        Cell r = new Cell(x);   // Optimistically create
                        // casCellsBusy() 更新 cellsBusy 为 1,通过 CAS 操作保证只有一个线程操作成功,cellsBusy() 方法相当于一个 spin lock
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) {
                                    // rs[j] 对象赋值
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            // Cell 在数组中赋值成功,跳出循环
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // CAS 操作成功,跳出循环
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) 
                    break;
                // 数组范围不能大于 JVM 可用核心数,cells != as 表示数组可能扩容
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                // 多个线程出现碰撞,更新 collide = true,出现碰撞后并没有直接扩容 cells 数组,而是重新 rehash,rehash CAS 失败后才会扩容
                else if (!collide) 
                       collide = true;
                // 走到这里说明出现了数组碰撞,且自旋 rehash CAS 失败,这时需要对数组扩容
                else if (cellsBusy == 0 && casCellsBusy()) { 
                    try {
                        // 数组扩容,重置 cells 数组
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    // 扩容完成后标记碰撞为 false
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 重置线程 threadLocalRandomProbe 值,重新 rehash 用
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        // 初始化 cells 数组,默认大小为 2
                        Cell[] rs = new Cell[2];
                        // 角标赋值
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 多个线程初始化,只有一个线程初始化成功,其他线程尝试更新 base 值
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

下面是具体每一个分支的详细说明:

  • cells 数组不为空,尝试更新数组中的 Cell 元素
    • 如果当前线程对应数组中的槽没有 Cell 元素,则初始化一个 Cell 元素,加锁成功后将初始化的 Cell 存在数组对应的槽中,跳出循环,槽位置 = thread.threadLocalRandomProbe & cells.length - 1,这里 & 操作相当于 %
    • 如果 wasUncontended 为 false,表示 CAS 操作失败,操作失败后会重置线程的 threadLocalRandomProbe 属性,自旋时会重新 rehash
    • CAS 操作当前数组槽对应的 Cell,累加操作的变量值,累加成功跳出循环,失败重置线程的 threadLocalRandomProbe 属性,自旋时会重新 rehash
    • cells 数组可能扩容,数组长度不能大于 JVM 的可用核心数,如果扩容,或者数组已经达到最大容量,将 collide 值置为 false
      • 这里补充说明一下,collide 为碰撞的意思,指的是多个线程经过 hash 后对应数组中的槽是否出现碰撞
      • 如果 cells 数组已经扩容到了最大限制,即使出现碰撞也不会再扩容 cells 数组了,因此将 collide 值置为 false
      • cells != as 表示数组出现了扩容,此时忽略碰撞情况,也将 collide 值置为 false
    • 如果 collide 为 false,将 collide 置为 true,意味着这此时已经出现了碰撞,出现碰撞并不会直接扩容 cells 数组,而是更新线程 threadLocalRandomProbe,自旋时重新 rehash,rehash CAS 失败后才会扩容
    • 如果出现了碰撞,且 rehash 后 CAS 更新 Cell 失败,进行加锁,加锁成功对 cells 数组扩容
  • cells 数组还没有初始化,且线程加锁成功,则初始化 cells 数组容量为 2,且将当前线程对应的 value 值封装成 Cell 元素,存储 cells 数组中
  • 可能有多个线程尝试初始化 cells 数组,但最终成功的只有一个,其他初始化失败的并不会以自旋的方式操作 cells 数组,而是尝试通过 CAS 去操作 base 值,因此在 cells 数组初始化完成之后,也是有可能是修改 base 值的

到这里 LongAdder 的原理就介绍完了,这时再来看以下几个问题?

  1. cells 数组初始化完成是不是就不会再更新 base 值了?

答:不会,可能有多个线程尝试初始化 cells 数组,最终只有一个线程成功,失败的线程还会以 CAS 的方式更新 base

  1. cells 数组什么时候扩容?

答:多个线程操作 cells 数组出现槽碰撞,碰撞后并不会直接扩容,而是修改线程的 threadLocalRandomProbe 值,以自旋的方式重新 rehash,如果还出现碰撞(此时 collide = true),则扩容 cells 数组

  1. cells 数组的最大容量是多少?

答:上面代码过程中有一个 else if (n >= NCPU || cells != as) 判断,这个 NCPU 表示 JVM 可用核心数,NCPU = Runtime.getRuntime().availableProcessors(); 。注意这个 JVM 可用核心数并不一定等于 CPU 核心数,比如我的电脑是 6 核,JVM 可用核心数是 12。else if (n >= NCPU || cells != as) 意味着数组的容量不能大于 JVM 的可用核心数,假设一个服务器 JVM 可用核心数为 6,由于数组每次扩容 2 倍,第一次初始化时为 2,那最大容量应该为 4。其实不是这样的,因为这个判断是在扩容前进行的,假设此时数组容量为 4,由于可用核心数为 6,条件判断通过,且存在碰撞情况,那么还是会扩容 cells 的容量为 8。因此我认为 cells 数组的最大容量为第一个大于 JVM 可用核心数的 2 的幂。

如果以上分析有错误分歧,欢迎大家在下面留言交流指正。

参考

LongAdder and LongAccumulator in Java

A Guide to False Sharing and @Contended

How do cache lines work?

JAVA 拾遗 — CPU Cache 与缓存行

Java并发工具类之LongAdder原理总结

<think>我们正在讨论Java中的LongAdder类,这是一个用于高并发场景下计数的工具。根据引用,LongAdder通过分散竞争来提升性能。接下来,我将详细解释其工作原理和实现机制。 首先,LongAdder的设计目标是减少线程竞争。它采用了一个基础值(base)和一个单元格数组(cells),当没有竞争时,操作直接作用于base;当出现竞争时,会使用cells数组来分散更新。 核心思想:将单个值的更新压力分散到多个值上,最后在需要总和时进行汇总。 具体实现步骤: 1. 初始时,LongAdder内部有一个base值,所有更新操作都直接尝试更新base(通过CAS操作)。 2. 当多个线程竞争更新base时(即CAS失败),它会初始化一个cells数组(每个元素是一个Cell对象,内部是一个volatile long值),并将更新操作分散到数组的不同元素上。 3. 每个线程根据哈希算法映射到cells数组的一个位置,然后更新该位置的Cell值。这样,多个线程就可以同时更新不同的Cell,从而减少竞争。 4. 当需要获取当前总和时,将base和所有cells数组中的值相加即可。 注意:Cell数组是惰性初始化的,只有在发生竞争时才创建。此外,数组的大小会根据竞争情况动态扩展(比如从2扩容到4、8等),但不会无限扩容,有一个上限(通常是CPU核心数的一个倍数,因为过核心数的线程数并不能真正并行)。 源码关键点(基于Java 17,具体实现可能因版本而异): - LongAdder继承自Striped64类,Striped64中实现了核心的分段累加逻辑。 - Cell类使用@Contended注解避免伪共享(缓存行共享问题)。 - 核心方法: increment() -> 调用add(1L) add(long x) -> 尝试更新base,失败则进入cell更新逻辑 sum() -> 返回base与所有cell值的总和(注意:没有加锁,所以是一个近似值,但在没有并发更新时是准确的) 下面是一个简化的代码逻辑(非完整源码,仅展示关键步骤): public void add(long x) { Cell[] cs; long b, v; int m; Cell c; // 首先尝试更新base if ((cs = cells) != null || !casBase(b = base, b + x)) { // 如果cells数组不为空(已经初始化过)或者更新base失败(说明有竞争),则进入 boolean uncontended = true; if (cs == null || (m = cs.length - 1) < 0 || (c = cs[getProbe() & m]) == null || !(uncontended = c.cas(v = c.value, v + x))) // 如果cells数组未初始化,或者当前线程对应的cell为空,或者更新cell失败,则调用longAccumulate方法 longAccumulate(x, null, uncontended); } } final boolean casBase(long cmp, long val) { return BASE.compareAndSet(this, cmp, val); } longAccumulate方法负责初始化cells数组,扩容,以及为线程分配cell等。 性能优势:在高并发下,多个线程可以同时更新不同的cell,从而大大减少竞争,提高吞吐量。但获取总和(sum)时,需要遍历整个cells数组,所以sum操作的开销相对较大,不适合频繁调用。 与AtomicLong比较:AtomicLong使用一个volatile变量,每次更新都需要CAS操作,高并发下CAS失败重试的开销大。而LongAdder通过分散热点,使得每个线程大部分时间操作自己独立的cell,竞争减少。 缺点:LongAdder在统计时可能会有精度损失(因为并发更新时,sum操作没有加锁,所以是某一时刻的快照,但最终一致)。另外,它占用更多的内存(cells数组)。 应用场景:适合高并发的计数,如统计请求次数、点击量等,但不适合需要精确实时获取值的场景(如控制流程)。 根据引用[4]中的例子,LongAdder常与ConcurrentHashMap结合使用,用于维护频率映射。 相关问题: §§相关问题§§ 1. LongAdder在哪些场景下比AtomicLong更有优势? 2. LongAdder的sum()方法为什么在并发更新时不是精确的? 3. 如何避免LongAdder中Cell数组的伪共享问题? 4. LongAdder的初始化和扩容机制是怎样的?</think>### Java LongAdder 类的工作原理及实现机制 `LongAdder` 是 Java 并发库(`java.util.concurrent.atomic`)中用于高效处理高并发计数场景的类。其核心设计目标是通过**分散竞争**来提升性能,避免 `AtomicLong` 在高并发下的性能瓶颈[^1][^2]。以下是详细实现机制: --- #### **1. 核心设计思想** - **分散热点**:将单一共享变量的更新压力分散到多个变量(称为 `Cell`)上,减少线程竞争[^1][^5]。 - **空间换时间**:通过额外内存开销换取更高吞吐量,适用于统计场景(如计数、直方图)而非精确同步控制[^3]。 - **最终一致性**:结果不保证实时精确,但调用 `sum()` 时可获得最终一致的总和[^1]。 --- #### **2. 关键数据结构** ```java // 简化版源码(基于 OpenJDK) public class LongAdder extends Striped64 { // 基础值(无竞争时直接更新) transient volatile long base; // 动态扩容的 Cell 数组(处理竞争) transient volatile Cell[] cells; // Cell 内部类(避免伪共享) @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } } } ``` - **`base`**:无竞争时的主计数器,通过 CAS 更新。 - **`cells` 数组**:当线程竞争 `base` 失败时,初始化数组并将更新分散到不同 `Cell` 元素[^5]。 - **`@Contended` 注解**:填充 `Cell` 的内存布局,防止 CPU 缓存行伪共享(False Sharing)[^5]。 --- #### **3. 工作流程** 以 `increment()` 为例: 1. **尝试更新 `base`** 使用 CAS 直接更新 `base`(低竞争时高效)。 ```java if (CAS(base, current, current + 1)) return; // 成功则退出 ``` 2. **竞争时转向 `cells` 数组** - 若 `cells` 未初始化,则创建大小为 2 的数组(惰性初始化)。 - 根据线程哈希值定位到某个 `Cell` 槽位: ```java int index = Thread.currentThread().hashCode() & (cells.length - 1); ``` - 对该 `Cell` 执行 CAS 更新(每个线程优先更新独立槽位)[^1][^5]。 3. **动态扩容** 若特定 `Cell` 槽位竞争仍激烈(CAS 失败),则扩容 `cells` 数组(通常翻倍),直到达到 CPU 核心数上限[^5]。 4. **求和(`sum()`)** 遍历所有 `Cell` 的值并累加到 `base`: ```java public long sum() { long sum = base; Cell[] cs = cells; if (cs != null) { for (Cell c : cs) if (c != null) sum += c.value; } return sum; // 注意:非原子快照 } ``` --- #### **4. 性能优势与代价** | **维度** | **LongAdder** | **AtomicLong** | |----------------|----------------------------------------|------------------------------| | **高并发吞吐量** | 极高(分散竞争)[^2][^5] | 低(CAS 竞争激烈时自旋开销大)| | **内存开销** | 较高(`Cell` 数组 + 填充)[^3][^5] | 低(单一变量) | | **结果精确性** | 最终一致(`sum()` 非原子)[^1] | 实时精确 | | **适用场景** | 统计计数(如 QPS 监控)[^4] | 细粒度同步控制 | --- #### **5. 典型应用场景** - **服务监控**:统计心跳请求次数(如引用[^4]的 `ServiceRegistry`)。 ```java Map<String, LongAdder> heartbeatCounters = new ConcurrentHashMap<>(); heartbeatCounters.computeIfAbsent(serviceId, k -> new LongAdder()).increment(); ``` - **频率统计**:与 `ConcurrentHashMap` 结合实现直方图(如引用[^3])。 - **高并发计数器**:替代 `AtomicLong` 用于访问量统计等[^2][^5]。 --- #### **6. 局限性** - **非实时精确**:`sum()` 遍历时可能遗漏正在更新的值[^1]。 - **内存敏感场景慎用**:`Cell` 数组可能占用较多内存[^3]。 - **不适用于原子操作**:如比较交换(`compareAndSet`)需用 `AtomicLong`[^3]。 --- ### 相关问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值