LongAdder/LongAccumulator类分析

文章详细介绍了Java1.8引入的LongAdder类,对比了其与AtomicLong的性能差异,并通过代码示例展示在高并发场景下LongAdder的优越性。LongAdder的性能提升主要源于其内部的Striped64结构,通过细粒度的锁减少冲突。文章还分析了LongAdder的add和longAccumulate方法,以及sum方法的无锁实现。

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

一、LongAdder简介

1.下图是JDK-API文档的的截图信息

我们可以得知这两个类都是1.8开始提供的,并且都具有顶级的并发性。这两类的区别点主要在于LongAdder初始值为0,只能做累加操作,而LongAccumulator可以完成一些复杂的计算,本文主要以LongAdder作为核心来讲解。
在这里插入图片描述在这里插入图片描述

2.LongAdder常用的api如下:

在这里插入图片描述

3.性能优势demo

class ClickNumber{
    int number = 0;
    public synchronized void clickBySynchronized(){
        number++;
    }
    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong(){
        atomicLong.getAndIncrement();
    }
    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder(){
        longAdder.increment();
    }
    LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
    public void clickByLongAccumulator(){
        longAccumulator.accumulate(1);
    }
}
public class Test {
 	public static final int _1W = 10000;
    public static final int threadNumber = 50;

    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;
        CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
       
        startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNumber; i++) {
            new Thread(() ->{
                try{
                    for (int i1 = 0; i1 < 100 * _1W; i1++) {
                        clickNumber.clickBySynchronized();
                    }
                }finally {
                    countDownLatch1.countDown();
                }
            }).start();
        }
        countDownLatch1.await();
        endTime =  System.currentTimeMillis();
        System.out.println("synchronize花费:"+(endTime - startTime) + "毫秒");
    }
}

在这里插入图片描述
其结果展示了在高并发的状态下,LongAdder和LongAccumulator完爆上面两种高并发累加,AutomicLong性能与之差了9-10倍。(因为其底层减少了乐观锁的重试次数)




二、原因分析

1.类继承分析

LongAdder是striped64的子类,该64类是性能暴虐其他原子类的灵魂
在这里插入图片描述
1.下图是Striped64的重要成员变量和方法解析
在这里插入图片描述
在这里插入图片描述
3.先说结论:
在这里插入图片描述
在这里插入图片描述

2.LongAdder类源码分析

1.分析add方法

在这里插入图片描述

 public void add(long x) {
	  //as是striped64中cells数组属性
	  //b是striped64中的base属性
	  //v是当前线程hash到的cell中存储的值
	  //m是cells的长度减1
	  //a是当前线程hash命中的cells单元格
      Cell[] as; long b, v; int m; Cell a;
      //首次首线程((as == cells) != null)一定是false,此时走casBase方法,以CAS的方式更新base值,且只有当cas失败时,才会走到if中
      //条件1: cells不为空
      //条件2: cas操作base失败,说明其他线程先一步修改了base,正在出现竞争
      if ((as = cells) != null || !casBase(b = base, b + x)) {
      	  // true无竞争 false表示竞争激烈,多个线程hash到同一个cell,可能要扩容
          boolean uncontended = true;
          //条件1:cells为空(第一次进来肯定符合条件,于是进行longAccumulate中创建cells[])
          //条件2:应该不会出现
          //条件3:当前线程所在的cell为空,说明当前线程还没有更新过cell,应该初始化一个cell
          //条件4:更新当前线程所在的cell失败,说明现在竞争很激烈,多个线程hash到了同一个cell,应扩容
          if (as == null || (m = as.length - 1) < 0 ||
          	  //getProbe()方法返回的是线程中threadLocalRandomProbe字段
          	  //它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非刻意修改它)
              (a = as[getProbe() & m]) == null ||
              !(uncontended = a.cas(v = a.value, v + x)))
              longAccumulate(x, null, uncontended);//调用striped64中的方法处理
      }
  }

总结:
1.如果cells表为空,尝试用CAS更新base字段,成功则退出
2.如果cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate(初始化大小为2的cells数组,并把当前线程hash命中的槽位初始化)
3.如果cells表非空,但当前线程映射的槽为空,uncontended为true,调用longAccumulate(初始化该槽位的cell)
4.如果cells表非空,且当前线程映射的槽非空,CAS更新cell的值,成功则返回,否则,uncontended设为false,调用longAccumulate(先尝试让当前线程换个槽再来一次cas,如果依旧失败则再换个槽来次cas,如果还失败就对cells数组进行扩容)

2.分析longAccumulate

在这里插入图片描述

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        // 存储线程的probe值                      
        int h;
        // 如果getProbe()方法返回0,说明随机数未初始化
        if ((h = getProbe()) == 0) { // 这个if相当于给当前线程生成一个非0的hash值
            // 使用 ThreadLocalRandom 为当前线程重新计算一个hash值,强制初始化
            ThreadLocalRandom.current(); // force initialization
            // 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true。
            h = getProbe();
            // 重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,wasUncontended竞争状态为true
            wasUncontended = true;
        }
        //如果hash取模映射到得到的cell单元不是null,则为true,此值也可以看作是扩容意向
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // CASE1: cells已经被初始化了
            if ((as = cells) != null && (n = as.length) > 0) {
                // 前面add方法中第二个if判断的条件3成立就会走下面的这个if里面的逻辑
                if ((a = as[(n - 1) & h]) == null) {     // 当前线程的hash值运算后映射得到的Cell单元为null,说明cell没有被使用
                    if (cellsBusy == 0) {                // cell[]数组没有正在扩容  Try to attach new Cell
                        Cell r = new Cell(x);            // 创建一个cell单元       Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {     // 尝试加锁,成功后 cellBusy == 1
                            boolean created = false;
                            try {                                   // 在有锁的情况下再检查一遍之前的判断  Recheck under lock
                                Cell[] rs; int m, j;                // 将cell单元附到Cell[]数组上
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // wasUncontended表示第一次CAS更新Cell单元是否成功了   CAS already known to fail
                    wasUncontended = true;      // 重新置为true,后面会调用”h = advanceProbe(h);“重新计算线程的hash值。由于此处没有break,再次循环会进入下面的if           Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))  // 尝试CAS更新cell单元格,cas成功则直接跳出循环(能进入改行说明当前线程对应的数组中有了数据,也重置过hash值)
                    break;
                else if (n >= NCPU || cells != as)                    // 当Cell数组的大小已经超过CPU核数后,永远不会再进行扩容
                    collide = false;                                  // 扩容标识,置为false,表示不会再进行扩容,并重新计算当前线程的hash值继续循环       At max size or stale
                else if (!collide)
                    collide = true;                                   // 如果扩容意向collide是false,则重新计算当前线程的hash值继续循环
                else if (cellsBusy == 0 && casCellsBusy()) {          // 尝试加锁进行扩容
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];             // 扩容后的大小 ==  当前容量 * 2
                            for (int i = 0; i < n; ++i)               // 扩容后再将之前数组的元素拷贝到新数组中
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 重置当前线程的hash值
                h = advanceProbe(h);
            }
            // CASE2:cells没有加锁且没有初始化,则尝试对它进行加锁,并初始化cells的值
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    // 如果不进行double check,就会再次new一个cell数组,上一个线程对应数组中的值将会被篡改
                    if (cells == as) {
                        // 新建一个大小为2的Cell数组
                        Cell[] rs = new Cell[2];
                        // 找到当前线程hash到数组中的位置并创建其对应的Cell,x的默认值是1
                        // h & 1 类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (tab.len - 1),同hashmap一个意思
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // CASE3: cells正在进行初始化,则尝试直接在基数base上进行累加操作
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

流程图总结:
在这里插入图片描述在这里插入图片描述

3.分析sum方法

发现底层很简单啊,直接把base和cells[]数组中的每个元素加起来,并且该方法没有加锁
在这里插入图片描述

三、最终总结表

AtomicLong:
在这里插入图片描述

LongAdder:
在这里插入图片描述

<think>好的,我现在需要给用户讲解一下关于Java中的Atomic。用户之前已经询问过逃逸分析,现在转向并发相关的Atomic,可能需要了解并发编程中的原子操作。首先,我得回忆一下Atomic包里的主要,比如AtomicInteger、AtomicReference、AtomicStampedReference等,以及它们的作用和原理。 用户可能对为什么需要Atomic有疑问,比如在多线程环境下,普通的变量操作可能无法保证原子性,导致数据不一致。这时候需要Atomic通过CAS(Compare-And-Swap)机制来保证原子操作。我需要解释CAS的工作原理,以及它在无锁算法中的应用。 接下来,应该分几个部分来组织内容:首先介绍Atomic的作用和解决的问题,然后详细说明CAS机制,接着列举常用的Atomic及其使用场景,可能还要比较不同的区别,比如AtomicStampedReference解决ABA问题。最后,可能需要提到Atomic的优缺点,以及在实际开发中的最佳实践。 需要注意的是,用户可能对底层实现不太熟悉,所以需要用浅显易懂的语言解释CAS,避免过于技术化。同时,结合代码示例会更有帮助,比如展示一个没有使用Atomic的线程不安全例子,再展示使用Atomic后的正确版本。此外,ABA问题及其解决方案也是关键点,需要详细说明。 还要考虑到用户可能想了解如何选择不同的Atomic,比如什么时候用AtomicInteger,什么时候用AtomicReference,以及Atomic数组的使用场景。此外,可以提到Java 8引入的累加器,如LongAdder,在并发量高时的性能优势。 最后,要确保回答结构清晰,层次分明,可能分为概述、CAS原理、常用介绍、ABA问题及解决、性能考量等部分。同时,检查是否有遗漏的重要点,比如内存可见性、volatile关键字与Atomic的关系,或者与synchronized的比较等。确保覆盖用户可能关心的各个方面,并提供足够的示例和实际应用场景,帮助用户更好地理解和应用Atomic。</think>以下是对Java原子(Atomic Classes)的详细讲解,采用分层结构便于理解: --- ### 一、原子存在的意义 **解决的问题**: 在多线程环境下,普通变量的非原子操作(如`i++`)会导致竞态条件(Race Condition)。例如: ```java int count = 0; // 线程不安全操作 public void unsafeIncrement() { count++; // 实际包含"读取-修改-写入"三个步骤 } ``` **传统解决方案**: - 使用`synchronized`关键字(带来线程阻塞开销) - 使用`volatile`关键字(仅保证可见性,不保证原子性) **原子的优势**: 通过**CAS(Compare And Swap)** 实现无锁并发,兼顾线程安全和性能。 --- ### 二、核心机制:CAS原理 ```java // 伪代码示意 public class AtomicInteger { private volatile int value; // 保证可见性 public final int getAndIncrement() { int oldValue; do { oldValue = value; // 步骤1:获取当前值 newValue = oldValue + 1; // 步骤2:计算新值 } while (!compareAndSwap(oldValue, newValue)); // 步骤3:CAS原子操作 return oldValue; } } ``` - **CAS操作**: `compareAndSwap(预期原值, 新值)` 仅当当前值等于预期原值时,才将值更新为新值(CPU硬件指令保证原子性) - **自旋(Spin)**: 若CAS失败,则循环重试直到成功 --- ### 三、常用原子 #### 1. 基础型原子 | 名 | 功能 | 示例方法 | |-----------------------|-----------------------------|-----------------------------| | `AtomicInteger` | 原子更新int型 | `getAndIncrement()` | | `AtomicLong` | 原子更新long型 | `addAndGet(delta)` | | `AtomicBoolean` | 原子更新boolean值 | `compareAndSet(true, false)`| #### 2. 引用型原子 | 名 | 特性 | |-----------------------|------------------------------| | `AtomicReference` | 原子更新对象引用 | | `AtomicStampedReference` | 带版本号的引用(解决ABA问题) | | `AtomicMarkableReference` | 用boolean标记状态的引用 | #### 3. 数组原子 | 名 | 功能 | |-----------------------|------------------------------| | `AtomicIntegerArray` | 原子更新int数组元素 | | `AtomicLongArray` | 原子更新long数组元素 | | `AtomicReferenceArray`| 原子更新对象引用数组元素 | #### 4. 字段更新原子 | 名 | 用途 | |-----------------------|------------------------------| | `AtomicIntegerFieldUpdater` | 原子更新对象的int字段 | | `AtomicLongFieldUpdater` | 原子更新对象的long字段 | | `AtomicReferenceFieldUpdater` | 原子更新对象的引用字段 | --- ### 四、解决ABA问题 **问题场景**: 线程1准备将值从A改为B,但在此期间线程2将A→C→A,线程1的CAS仍然成功(无法感知中间变化) **解决方案**: ```java // 使用AtomicStampedReference AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); // 线程1尝试修改 int stamp = ref.getStamp(); ref.compareAndSet("A", "B", stamp, stamp + 1); // 线程2修改后恢复 ref.set("A", ref.getStamp() + 1); // 版本号变化 ``` --- ### 五、JDK8增强 | 名 | 适用场景 | 优势 | |-------------------|--------------------------|-----------------------------| | `LongAdder` | 高并发统计场景(如计数器) | 分段累加,减少CAS竞争 | | `LongAccumulator` | 自定义累加规则 | 支持lambda表达式(如max/min) | **性能对比**: ```java // 测试代码片段 LongAdder adder = new LongAdder(); IntStream.range(0, 1000).parallel().forEach(i -> { adder.increment(); }); ``` --- ### 六、使用建议 1. **优先选择基础原子** 简单计数场景使用`AtomicInteger`/`LongAdder` 2. **警惕过度自旋** CAS失败频繁时考虑锁升级策略 3. **组合操作注意** ```java // 错误示例:两个独立CAS不能保证原子性 if (atomicA.get() == expectedA) { atomicB.set(newB); // 此时atomicA可能已被修改 } // 正确做法:使用AtomicReference打包多个变量 class Wrapper { A a; B b; } AtomicReference<Wrapper> ref = new AtomicReference<>(); ``` 4. **内存可见性保障** 所有原子的get()方法都具有volatile读语义 --- ### 七、底层实现对比 | 实现方式 | 示例 | 特点 | |-------------|--------------------|-----------------------------| | **CAS** | AtomicInteger | 无锁,适合低竞争场景 | | **synchronized** | Vector | 悲观锁,高竞争下稳定但吞吐量低 | | **分段锁** | ConcurrentHashMap | 降低锁粒度,平衡竞争与复杂度 | > **最终建议**:在80%的并发场景中,原子能提供更好的性能,但在超高并发(如万级TPS)或复杂原子操作时,需结合具体场景选择锁或并发容器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

键盘歌唱家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值