LongAdder使用及原理浅析(对比AtomicLong进行分析)

概述

        LongAdder是JDK1.8之后新引入的原子类,可应用于高并发写共享变量场景(以下翻译摘自源码):

当多个线程更新一个用于统计等目的(而非用于细粒度同步控制)的公共总和时,此类通常比AtomicLong更可取。在低更新争用情况下,这两个类的特性相似。但在高争用情况下,此类的预期吞吐量显著提高,但代价是更高的空间消耗。 LongAdder可以与ConcurrentHashMap一起使用,以维护一个可扩展的频率映射(一种直方图或多重集合的形式)。例如,要向`ConcurrentHashMap<String,LongAdder> freqs`中添加一个计数(如果尚不存在则进行初始化),可以使用`freqs.computeIfAbsent(k -> new LongAdder()).increment();`。

        理解LongAdder之前,可以通过与AtomicLong进行对比,以便更好地理解LongAdder。

AtomicLong工作原理

         AtomicLong是Java并发包(java.util.concurrent.atomic)中的一个类,提供了一种线程安全的方式来进行长整型数据的原子操作。其底层工作原理主要基于CAS(Compare-And-Swap)操作,以下是对其底层工作原理的详细解释:

一、CAS操作

CAS(Compare-And-Swap)操作是一种基于硬件的原子操作,可以在多线程环境下实现对共享变量的原子访问。它包含了三个操作数:

  1. 内存地址V:表示要更新的变量在内存中的地址。
  2. 预期值A:表示如果内存地址V中的当前值等于A,才进行更新操作。
  3. 新值B:表示如果内存地址V中的当前值等于A,则将V的值更新为B。

CAS操作的执行流程如下:

  1. 比较内存地址V中的当前值是否等于预期值A。
  2. 如果相等,则将内存地址V的值更新为新值B。
  3. 如果不相等,则不做任何操作。

CAS操作是无锁的,因此可以避免传统锁机制带来的性能开销。但是,由于CAS操作是基于硬件实现的,其性能可能会受到硬件平台和操作系统的影响。

二、AtomicLong的CAS实现

        AtomicLong内部使用一个volatile修饰的long型变量来存储数值,这个变量用于保存被操作的长整型值。AtomicLong通过sun.misc.Unsafe类来实现底层的CAS操作。Unsafe类提供了一些底层操作,如直接访问内存、修改对象字段等。这个类在JDK中并不是公共API,因此使用时需要谨慎。

AtomicLong的CAS实现流程如下:

  1. 当线程尝试更新AtomicLong的值时,会先获取当前内存地址上的值。
  2. 将这个值与预期值进行比较。
  3. 如果相等,则使用CAS操作将新值写入内存地址。
  4. 如果不相等,则说明有其他线程已经更新了该值,此时当前线程会重新获取内存地址上的新值,并再次进行比较和更新操作,直到成功为止。

        这种基于CAS的更新方式可以确保对变量的读写操作具有原子性,并且能够利用底层硬件的原子指令或者类似机制来达到高效的并发处理。

三、AtomicLong的性能问题

        在高并发场景下,AtomicLong的性能可能会受到一定影响,因为多个线程同时尝试执行CAS操作时,只有一个线程能够成功。其他线程在CAS操作失败后,会通过循环重试的方式再次尝试,直到成功为止。这种循环重试机制可能会导致CPU资源的浪费和性能下降。

        因此为了解决AtomicLong的CAS自旋造成的CPU空转消耗问题,Doug Lea大神在JDK1.8开始为我们引出了LongAddr原子类。

LongAdder工作原理

        旨在解决高并发环境下Atomic类频繁CAS(Compare-And-Swap)操作导致的CPU资源浪费问题。其工作原理主要基于分段锁机制,通过牺牲一定的空间来换取更高的时间效率,即在多线程环境中提高性能。以下是LongAdder工作原理的详细解释:

一、核心思想

        LongAdder的核心思想是分散热点,将原本由单个变量(如AtomicLong中的value)承担的更新压力分散到多个变量(即LongAdder中的Cell数组)中去。这样,不同线程会命中数组的不同槽位,各自只对自己槽位的值进行CAS操作,从而大大降低了并发冲突的概率。

二、主要成员变量

LongAdder继承了Striped64类,并主要利用了以下几个关键成员变量:

  1. base:一个volatile修饰的long类型变量,用于在无竞争或低竞争情况下存储总和值。
  2. cells:一个volatile修饰的Cell[]数组,用于在高竞争情况下存储分段的总和值。Cell是Striped64的一个静态内部类,其中包含一个volatile long value变量来存储分段值。
  3. cellsBusy:一个volatile int变量,用于指示cells数组是否正在被初始化或扩容。它有两个值:0表示无锁状态,1表示其他线程已经持有了锁。

三、工作原理

  1. 无竞争情况

    • 在最初无竞争时,LongAdder只更新base的值。此时,add方法会尝试通过CAS操作将值累加到base上。
  2. 出现竞争

    • 当多个线程同时尝试更新base值时,CAS操作可能会失败。此时,LongAdder会创建cells数组,并将更新压力分散到数组的各个Cell中。
    • 每个线程会根据其线程ID的哈希值映射到cells数组的某个下标,然后对该下标所对应的Cell值进行CAS操作。
    • 如果某个Cell尚未初始化,则当前线程会尝试创建该Cell,并将其值设置为需要增加的值。这个过程需要获取cellsBusy锁来确保线程安全。
  3. 扩容机制

    • 当cells数组中的竞争变得非常激烈时(例如,数组长度达到了CPU核心数或扩容阈值),LongAdder会尝试扩容cells数组。
    • 扩容过程也需要获取cellsBusy锁,并创建一个新的、长度是原数组两倍的新数组。然后,将旧数组中的元素复制到新数组中,并更新cells引用为新数组。
  4. 获取总和

    • LongAdder的sum方法会遍历cells数组,并将所有Cell的值和base值相加,得到最终的总和值。

LongAdder使用示例

package com.test;

import java.util.concurrent.atomic.LongAdder;

/**
 * @ClassName : LongAdderTest
 * @Description:
 * @Author: liulianglin
 * @Date: 2024-10-26 10:10
 * @Version : 1.0
 */
public class LongAdderTest {


    public static void main(String[] args) {

        LongAdder longAdder = new LongAdder();
        for (int i =0; i< 100; i++){
            new Thread(()->{

                // 下面的代码可以在多线程中安全进行,不用考虑加锁
                longAdder.increment();

            }, " thread-"+i).start();
        }

        while(Thread.activeCount() > 2){
            // 当活跃线程数大于设定的默认线程数的时候 主线程让步
            Thread.yield();
        }
        // 当前累加器的值
        long sum = longAdder.sum();
        System.out.println(sum);

    }
}

执行结果:

### 使用 `LongAdder` 进行并发计数的教程与示例代码 `LongAdder` 是 Java 提供的一个高效的并发计数工具,适用于高并发场景下的计数需求。与 `AtomicLong` 相比,`LongAdder` 通过分段更新策略减少了线程竞争,从而提升了性能。以下是如何使用 `LongAdder` 进行并发计数的详细教程和示例代码。 #### 基本用法 `LongAdder` 提供了简单易用的 API 来执行计数操作。主要的方法包括: - `increment()`:将计数器增加 1。 - `add(long x)`:将指定值添加到当前计数器。 - `sum()`:获取当前的总值。 - `reset()`:重置计数器为 0。 以下是一个简单的示例,展示了如何使用 `LongAdder`: ```java import java.util.concurrent.atomic.LongAdder; public class LongAdderExample { public static void main(String[] args) { // 创建LongAdder实例 LongAdder longAdder = new LongAdder(); // 执行累加操作 longAdder.increment(); // 增加1 longAdder.increment(); // 再次增加1 longAdder.add(10); // 增加10 // 获取当前累加的总值 long sum = longAdder.sum(); System.out.println("总计数值: " + sum); // 输出:12 // 重置LongAdder longAdder.reset(); System.out.println("重置后总值: " + longAdder.sum()); // 输出:0 } } ``` #### 高并发场景下的使用 在高并发环境下,多个线程同时对计数器进行操作时,`LongAdder` 的性能优势尤为明显。以下是一个多线程并发操作 `LongAdder` 的示例: ```java import java.util.concurrent.atomic.LongAdder; public class ConcurrentLongAdderExample { public static void main(String[] args) throws InterruptedException { // 创建LongAdder实例 LongAdder longAdder = new LongAdder(); // 创建并启动10个线程 Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { // 每个线程执行1000次加法操作 for (int j = 0; j < 1000; j++) { longAdder.increment(); } }); threads[i].start(); } // 等待所有线程执行完毕 for (Thread thread : threads) { thread.join(); } // 输出结果 System.out.println("Sum: " + longAdder.sum()); // 输出:10000 } } ``` 在这个示例中,10 个线程各自执行 1000 次 `increment()` 操作,最终的总值为 10000。由于 `LongAdder` 内部采用了分段更新策略,多个线程可以并行操作不同的段,从而减少了线程竞争,提高了并发性能。 #### 与 `AtomicLong` 的对比 与 `AtomicLong` 不同,`LongAdder` 并不保证中间值的实时一致性。`sum()` 方法返回的是当前的近似值,而不是精确值。在高并发场景中,这种设计可以显著提升性能[^2]。 #### 适用场景 - **高并发计数**:适用于多个线程频繁更新计数的场景,例如统计请求次数、日志计数等。 - **最终一致性要求**:如果业务逻辑可以接受最终一致性的结果,而不是实时的精确值,`LongAdder` 是一个理想的选择。 #### 注意事项 - **避免频繁调用 `sum()`**:频繁调用 `sum()` 方法可能会导致性能下降,因为每次调用都需要合并所有段的值。 - **线程安全**:`LongAdder` 是线程安全的,无需额外的同步措施。 ### 总结 `LongAdder` 是一个高效的并发计数器,特别适合高并发场景。通过分段更新策略,它能够有效减少线程竞争,提升性能。开发者在使用时应根据具体需求选择合适的并发工具类,并理解其适用场景和限制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值