比AtomicLong更高效的并发计数类LongAdder

本文探讨了在并发环境下,LongAdder如何优化AtomicLong的性能。AtomicLong通过CAS操作保证线程安全,但在高并发下可能导致CPU开销过大。LongAdder则利用Cell对象和线程局部随机值减少冲突,适用于高并发统计。总结指出,选择使用AtomicLong还是LongAdder取决于具体的应用场景和并发水平。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在看https://github.com/alibaba/Sentinel(轻量级的流量控制、熔断降级 Java 库)源码的时候,看到在统计数量的时候使用了LongAdder。这个LongAdder是jdk1.8新增的,出自Doug Lea之手,伟大的Java并发大师的鼻祖。在没有接触到LongAdder之前,AtomicLong这个类在并发计数上无论性能还是准确性已经做得极好了。在阿里流控框架中使用这样一个LongAdder类,必然有其过人之处。

回顾AtomicLong

AtomicLong是通过CAS(即Compare And Swap)原理来完成原子递增递减操作,在并发情况下不会出现线程不安全结果。AtomicLong中的value是使用volatile修饰,并发下各个线程对value最新值均可见。我们以incrementAndGet()方法来深入。

 	 public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

这里是调用了unsafe的方法

    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

方法中this.compareAndSwapLong()有4个参数,var1是需要修改的类对象,var2是需要修改的字段的内存地址,var6是修改前字段的值,var6+var4是修改后字段的值。compareAndSwapLong只有该字段实际值和var6值相当的时候,才可以成功设置其为var6+var4。

multiple-thread-cas-show1

再继续往深一层去看

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

这里Unsafe.compareAndSwapLong是native方法,底层通过JNI(Java Native Interface)来完成调用,实际就是借助C来调用CPU指令来完成。

实现中使用了do-while循环,如果CAS失败,则会继续重试,直到成功为止。并发特别高的时候,虽然这里可能会有很多次循环,但是还是可以保证线程安全的。不过如果自旋CAS操作长时间不成功,竞争较大,会带CPU带来极大的开销,占用更多的执行资源,可能会影响其他主业务的计算等。

LongAdder怎么优化AtomicLong

Doug Lea在jdk1.5的时候就针对HashMap进行了线程安全和并发性能的优化,推出了分段锁实现的ConcurrentHashMap。一般Java面试,基本上离不开ConcurrentHashMap这个网红问题。另外在ForkJoinPool中,Doug Lea在其工作窃取算法上对WorkQueue使用了细粒度锁来较少并发的竞争,更多细节可参考我的原创文章ForkJoin使用和原理剖析。如果已经对ConcurrentHashMap有了较为深刻的理解,那么现在来看LongAdder的实现就会相对简单了。

来看LongAdder的increase()方法实现,

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        //第一个if进行了两个判断,(1)如果cells不为空,则直接进入第二个if语句中。(2)同样会先使用cas指令来尝试add,如果成功则直接返回。如果失败则说明存在竞争,需要重新add
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            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);
        }
    }

这里用到了Cell类对象,Cell对象是LongAdder高并发实现的关键。在casBase冲突严重的时候,就会去创建Cell对象并添加到cells中,下面会详细分析。

    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        //提供CAS方法修改当前Cell对象上的value
        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);
            }
        }
    }

而这一句a = as[getProbe() & m]其实就是通过getProbe()拿到当前Thread的threadLocalRandomProbe的probe Hash值。这个值其实是一个随机值,这个随机值由当前线程ThreadLocalRandom.current()产生。不用Rondom的原因是因为这里已经是高并发了,多线程情况下Rondom会极大可能得到同一个随机值。因此这里使用threadLocalRandomProbe在高并发时会更加随机,减少冲突。更多ThreadLocalRandom信息想要深入了解可关注这篇文章并发包中ThreadLocalRandom类原理浅尝。拿到as数组中当前线程的Cell对象,然后再进行CAS的更新操作,我们在源码上进行分析。longAccumulate()是在父类Striped64.java中。

multiple-thread-cas-show2.png


    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
        	  //如果当前线程的随机数为0,则初始化随机数
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            //如果当前cells数组不为空
            if ((as = cells) != null && (n = as.length) > 0) {
            		//如果线程随机数对应的cells对应数组下标的Cell元素不为空,
                if ((a = as[(n - 1) & h]) == null) {
                		//当使用到LongAdder的Cell数组相关的操作时,需要先获取全局的cellsBusy的锁,才可以进行相关操作。如果当前有其他线程的使用,则放弃这一步,继续for循环重试。
                    if (cellsBusy == 0) {       // Try to attach new Cell
                    	//Cell的初始值是x,创建完毕则说明已经加上
                        Cell r = new Cell(x);   // Optimistically create
                        //casCellsBusy获取锁,cellsBusy通过CAS方式获取锁,当成功设置cellsBusy为1时,则获取到锁。
                        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] = r;
                                    created = true;
                                }
                            } finally {
                            	  //finally里面释放锁
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //如果a不为空,则对a进行cas增x操作,成功则返回    
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                //cells的长度n已经大于CPU数量,则继续扩容没有意义,因此直接标记为不冲突
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                //到这一步则说明a不为空但是a上进行CAS操作也有多个线程在竞争,因此需要扩容cells数组,其长度为原长度的2倍
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        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;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //继续使用新的随机数,避免在同一个Cell上竞争
                h = advanceProbe(h);
            }
            //如果cells为空,则需要先创建Cell数组。初始长度为2.(个人理解这个if放在前面会比较好一点,哈哈)
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //如果在a上竞争失败,且扩容竞争也失败了,则在casBase上尝试增加数量
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }
    

最后是求LongAdder的总数,这一步就非常简单了,把base的值和所有cells上的value值加起来就是总数了。

    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

思考

源码中Cell数组的会控制在不超过NCPU的两倍,原因是LongAdder其实在底层是依赖CPU的CAS指令来操作,如果多出太多,即使在代码层面没有竞争,在底层CPU的竞争会更多,所以这里会有一个数量的限制。所以在LongAdder的设计中,由于使用到CAS指令操作,瓶颈在于CPU上。

YY一下,那么有没有方式可以突破这个瓶颈呢?我个人觉得是有的,但是有前提条件,应用场景极其有限。基于ThreadLocal的设计,假设统计只在一个固定的线程池中进行,假设线程池中的线程不会销毁(异常补充线程的就暂时不管了),则可以认为线程数量是固定且不变的,那么统计则可以依赖于只在当前线程中进行,那么即使是高并发,就转化为ThreadLocal这种单线程操作了,完全可以摆脱CAS的CPU指令操作的限制,那么性能将极大提升。

总结

在并发处理上,AtomicLong和LongAdder均具有各自优势,需要怎么使用还是得看使用场景。看完这篇文章,其实并不意味着LongAdder就一定比AtomicLong好使,个人认为在QPS统计等统计操作上,LongAdder会更加适合,而AtomicLong在自增控制方面是LongAdder无法代替的。在多数地并发和少数高并发情况下,AtomicLong和LongAdder性能上差异并不是很大,只有在并发极高的时候,才能真正体现LongAdder的优势。

<think>我们正在回答用户关于Java LongAdder类的问题。根据引用内容,我们可以总结如下: LongAdderJava中用于高并发环境下高效计数的类。它通过分段累加(也称为Cell数组)的方式减少线程竞争,从而在并发场景下提供比AtomicLong好的性能。 主要功能: - 提供高效的累加操作(increment()和add()方法) - 支持获取当前总和(sum()方法) 使用场景: - 高并发计数,如统计访问次数、服务心跳计数等(如引用[3]中的服务注册中心心跳计数) - 需要低竞争、高吞吐的累加操作 注意事项(避坑指南,来自引用[1]): 1. 不要滥用LongAdder:在低并发场景下,AtomicLong可能合适(因为LongAdder占用多内存) 2. sum()方法返回的是调用时刻的近似值(因为计算总和时可能有其他线程正在新) 3. 避免过度使用reset()(因为它会重置计数器,可能导致数据丢失) 4. 不能用于需要CAS(Compare-And-Swap)语义的场景(因为LongAdder没有提供类似AtomicLong的compareAndSet方法) 与AtomicLong的区别(引用[2]): - LongAdder通过分散热点,减少竞争,提高并发性能 - AtomicLong通过CAS保证原子性,但在高竞争下性能下降 代码示例(参考引用[3]): ```java import java.util.concurrent.atomic.LongAdder; public class LongAdderExample { public static void main(String[] args) { LongAdder counter = new LongAdder(); // 多个线程并发累加 for (int i = 0; i < 10; i++) { new Thread(() -> { counter.increment(); // 相当于add(1) }).start(); } // 等待所有线程完成 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Total count: " + counter.sum()); } } ``` 在这个示例中,多个线程并发调用`increment()`方法,由于LongAdder内部的分段设计,减少了竞争,因此在高并发下性能好。 总结:LongAdder适用于高并发累加操作,但需要注意其精度问题和内存消耗。在需要精确原子操作(如序列号生成)时,可能仍需使用AtomicLong。</think>### Java LongAdder 类详解 #### 一、核心功能 `LongAdder` 是 Java 8 引入的高性能计数器类,专为**高并发累加场景**设计。其核心功能包括: 1. **原子累加**:通过 `increment()` 和 `add(long x)` 实现线程安全的数值累加 2. **低竞争求和**:通过 `sum()` 获取当前总值,在并发环境下性能显著优于 `AtomicLong` 3. **分段计数**:内部采用 **Cell 数组**分散竞争热点,减少线程冲突 #### 二、适用场景 1. **高频计数器**(最佳场景): - 网站访问量统计 - API 调用次数统计 - 服务心跳计数器(如引用[3]的服务注册中心) ```java // 服务心跳计数实现 public void receiveHeartbeat(String serviceId) { heartbeatCounters.get(serviceId).increment(); // 高并发高效累加 } ``` 2. **实时监控指标**: - 交易系统成交量统计 - 日志处理系统的消息计数 3. **非精确统计场景**: - 大数据采样统计 - 实时仪表盘数据(可接受瞬时误差) #### 三、性能优势(对比 AtomicLong) | 指标 | LongAdder | AtomicLong | |---------------|-------------------|-----------------| | 高并发写入性能 | ⭐⭐⭐⭐⭐ (分段CAS) | ⭐⭐ (全局CAS竞争) | | 内存占用 | 较高 (Cell 数组) | 较低 | | sum() 实时性 | 弱一致性 | 强一致性 | | 适用场景 | 写多读少 | 读多写少 | > 💡 **性能原理**:通过空间换时间,将单一计数器拆分为多个 Cell,线程竞争时只在各自 Cell 上 CAS 操作(引用[1][2]) #### 四、使用示例 ```java import java.util.concurrent.atomic.LongAdder; public class CounterDemo { private final LongAdder totalRequests = new LongAdder(); // 处理请求时累加 public void handleRequest() { processRequest(); totalRequests.increment(); // 无竞争累加 } // 获取当前统计值 public long getTotalRequests() { return totalRequests.sum(); // 弱一致性结果 } } ``` #### 五、避坑指南(引用[1]) 1. **勿滥用**:低并发场景用 `AtomicLong` 节省内存 2. **精度问题**:`sum()` 非原子快照,可能返回瞬时近似值 3. **慎用 reset()**:重置操作会丢失数据,通常用 `new LongAdder()` 替代 4. **禁用 CAS 场景**:不支持 `compareAndSet()` 等原子条件操作 > ⚠️ **典型误用**: > 需要精确原子操作时(如序列号生成),应选择 `AtomicLong` 而非 `LongAdder` #### 六、选型建议 - ✅ 选 `LongAdder`:高频写入 + 可接受弱一致性读取(如监控统计) - ✅ 选 `AtomicLong`:低频写入 + 需要强一致性读取(如资源控制) --- ### 相关问题 1. `LongAdder` 的 `sum()` 方法为何返回弱一致性结果?其底层如何实现分段累加? 2. 在分布式系统中,如何基于 `LongAdder` 设计服务健康检查机制(如引用[3])? 3. `LongAdder` 与 `ConcurrentHashMap` 结合使用时有哪些性能优化技巧? 4. 为什么 `LongAdder` 不适合用于 `compareAndSet` 场景?这种限制会带来哪些影响? 5. 如何通过 JMH 基准测试量化 `LongAdder` 和 `AtomicLong` 在高并发下的性能差异?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值