7.Atomic系列之LongAdder的底层原理(分段锁提升并发性能)

LongAdder是Java中为解决AtomicInteger在高并发下因CAS操作导致的线程自旋问题而设计的。它利用分段锁机制,将竞争分散到多个部分,从而减少并发冲突,提高性能。文章通过银行办事大厅的多窗口比喻,解释了分段锁的工作原理,并详细分析了LongAdder的源码实现。

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

Atomic系列之LongAdder的底层原理(分段锁提升并发性能)

这一章节我们就来讲解CAS带来的另外一个问题,在并发激烈的时候,产生大量的自旋,空耗CPU的问题,以及怎么使用分段锁机制解决这个问题的,我们以LongAdder这个原子类来举例讲解。

CAS空耗CPU问题

AtomicInteger的缺陷:并发竞争激烈时导致大量线程自旋

为什么要设计出LongAddr?

并发非常高的时候,使用AtomicInteger、AtomicLong底层的CAS操作竞争非常激烈,会导致大量线程CAS失败而不断自旋,耗费CPU的性能

unsafe类的getAndAddInt方法,源码如下:

public final int getAndAddInt(Object o, long valueOffset, int x) {
    int expected;
    // CAS的失败的线程,会不断在do... while循环里重试,知道成功为止
    do {
        expected = this.getIntVolatile(o, valueOffset);
        // while条件判断这里的compareAndSwapInt每次只有一个线程成功
    } while(!this.compareAndSwapInt(o, valueOffset, expected, expected + x));
    
    return expected;
}

(1)我们看一下上述 do…while 循环,假设线程有10000线程并发的操作,由于CAS操作能保证原子性(之前的文章讲过哦,CAS的底层原理),一个时刻只会有一个线程执行compareAndSwapInt方法成功

(2)失败的另外9999个线程,进入下一次循环,然后成功一个剩下9998个进入下一层循环…,这种方式啊,竞争太过激烈导致**大量线程****在循环里面自旋重试,此时是不会释放CPU资源的,大大降低CPU的利用率,降低了并发的性能。

分段锁思想减少并发竞争

LongAdder采用分段锁的思想**,去减少并发竞争的;我打个比方还是上面10000线程并发操作,但是LongAdder内部可能有10个锁,不同的线程可能去竞争不同的锁,平均下来可能是1000个线程竞争1个锁这样;并发性能这样比起AtomicInteger可能就**提升了10倍。

下面我以银行办事大厅多窗口机制来给你将什么是分段锁

如下图所示:

(1)银行大厅有一个常规窗口一堆备用窗口;每个窗口有个计数器value记录了当前窗口访客人数

(2)当人数很少的时候,不存在竞争,通常办事大厅只启用常规窗口就足以应对了

(3)当银行某个经理想知道总的访客人数的时候,需要将所有窗口的value访问人数累加就可以了

访客人数很少,比如一两个的时候只开放常规窗口就可以了。

同时每个窗口内部有一个访客计数器,窗口每次接待完一个访客,value就加1,当银行经理想看一下整个办事大厅的来访总人数的时候,就把所有的窗口的访客计数器value累加就可以了。

多窗口处理减少竞争

(1)在访客非常多的情况下,只开启常规窗口是不行的**,由于一个窗口同一时间只能处理一个访客的请求,并发竞争非常激烈(**这个就是上述说的AtomicInteger的缺陷,所有人竞争通过一个窗口)

(2)所以这个时候启用备用窗口,假如说有4个备用窗口,分别为A、B、C、D窗口**,每个用户自己有一个id,比如用户2(id=4)来了之后看到常规窗口有人了,就会根据 id % 备用窗口大小 =》 4 % 4 = 0,就会派到第一个备用窗口,也就是**窗口A

(3)同时并发非常激烈的时候同一个备用窗口也是存在竞争的,比如**用户3(id=5)、用户6(id=9)根据 id % 备用窗口大小 = 2,所以都竞争备用窗口B;**用户4(id=6)和用户7(id=10)**同样会竞争备用窗口C

(4)使用了备用窗口列表,相当于将不同的用户派遣到不同的窗口减少了竞争,这样并发性能自然就提升上去了。
在这里插入图片描述

上面的这种机制啊,其实就是类似将锁拆成锁多个段;其中每个窗口就是一段锁,只有被派到同一个窗口的用户才会存在竞争,使用分段锁大大减少了竞争,提升了并发性能。

这个时候如果银行经理想要知道办事大厅的总访问人数,只需要将多个窗口(多个段)内的value人数累加起来就可以了。

LongAdder分段索实现原理

首先,我给你说LongAdder之前得给你介绍一下Striped64这个类Striped64这个类是分段锁的基础,实现分段锁的功能,而LongAdder继承Striped64,只是在Striped64基础之上做了简单的封装而已。

Cell单元(窗口)

Cell {
    // 计数器,当前访问人数多少
    volatile long value;
    // 构造函数
    Cell(long x) { value = x; }
    // CAS竞争同一个窗口,可能多个用户CAS竞争这个窗口
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

基础窗口

直接用一个base变量表示基础窗口

transient volatile long base;

争抢基础窗口方法casBase底层源码,直接cas修改base变量在内存的值

final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

用户id生成方法

这里就是通过线程id,经过系列运算得到用户id

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

LongAdder内部源码分析

首先看下LongAdder提供的加减方法

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

public void decrement() {
    add(-1L);
}

我们看到,increment和decrement方法底层调用的都是add方法,只不过传入的是正数和负数的区别而已。

看一下设计极为简洁和精妙的add方法

public void add(long x) {
    // as就类似我们上述说的备用窗口列表
    Cell[] as;
    // 这里的b就是常规窗口的值,v是你被分派到的那个窗口的value值
    long b, v;
    // m是备用窗口的长度-1,
    // 上面我们讲过getProbe()方法就是获取用户id的方法
    // getProbe() & 其实就是 用户id % 窗口总数,窗口分派的算法
    int m;
    // a就是你被派遣到的那个窗口
    Cell a;
    // 1.首先如果cells==null,说明备用窗口没有开放,
    // 全都执行casBase争抢常规窗口,cas成功则争抢成功,然后办完事就退出了
    // 如果争抢失败 casBase == false,则会进入if代码内重试
    // 2. 如果cells != null,说明备用窗口开发了,不用去争抢常规窗口了,
    // 直接就进入争抢备用窗口
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        // 3. as == null || as.length - 1 < 0 说明备用窗口列表尚未开放
        if (as == null || (m = as.length - 1) < 0 ||
            // 4. as[getProbe() & m] 是你被派遣到的那个备用窗口
            // (a = as[getProbe() & m]) == null 你被派遣到的那个备用窗口还没有人来
            (a = as[getProbe() & m]) == null ||
            // 5. a.cas() 就是你被分派到窗口a后,去尝试争抢窗口a的权限
            // 如果 uncontented就是你争抢的结果,如果!uncnotented == true,说明你争抢失败了
            !(uncontended = a.cas(v = a.value, v + x)))
            
            // 相当于上面操作都失败之后的一种兜底方案
            longAccumulate(x, null, uncontended);
    }
}


Strimped64分段索实现源码

继续查看LongAccumulate的方法源码:

longAccumulate里面做了几件事情,先从大角度分析一下:

(1)进入方法,黄色部分,首先就是获取一下用户id,如果为0,先强制初始化一下用户id

(2)然后就是进入for死循环里面,只有用户的请求成功处理了,才会退出循环

(3)然后就是下面比较重要的三个大的分支条件

进入备用窗口处理

// cells是备用窗口列表,如果备用窗口不等于null,
// 并且是length>0 说明备用窗口开启了
// 则用户进入这个条件,去备用窗口列表里面处理
if ((as = cells) != null && (n = as.length) > 0)

初始化备用窗口

如果上面的分支不满足,说明 cells == null 或者 cells.length <= 0 说明备用窗口没开启啊,这个时候就要开启一下备用窗口,即进行备用窗口列表数组的初始化工作:

// cellsBusy 是备用窗口列表是否在初始化或者扩容标志
// cellsBusy == 0 说明尚未有人初始化或者扩容
// 这时候执行到这里的线程,就要进行对备用窗口初始化了
// 比如说线程1走到这里,发现没初始化,就要执行一下casCellBusy告诉别人正在初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy())

竞争常规窗口

如果cellsBusy == 1 说明有人在初始化备用窗口或者对备用窗口列表进行扩容,这个时候备用窗口列表不可用,只能去常规窗口争抢了

// 直接对常规窗口争抢,如果成功就退出死循环了
else if (casBase(v = base, ((fn == null) ? v + x :
                            fn.applyAsLong(v, x))))
    break;

这里画个图看下

(1)比如用户1进入了这里之后,首先判断分支1,发现备用窗口没有启用;然后直接****进入分支2,去初始化备用窗口

(2)然后用户2也进来了,发现备用窗口没启用,同时发现有人在初始化备用窗口,直接进入分支3,去争抢常规窗口了

(3)备用窗口初始化好了,开启了之后用户的请求进来都走到分支1,去备用窗口列表去竞争去了。

分支2的源码:初始化备用窗口

else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    // init是否初始化完成,最开始设置为false
    boolean init = false;
    try { // Initialize table
        // 然后这里就没什么复杂的,就是创建一个数组
        // 但是这个时候数组里面没有元素,每个元素都是空的
        if (cells == as) {
            Cell[] rs = new Cell[2];
            // 创建一个新的窗口,把自己的操作数x交给新创建的窗口
            rs[h & 1] = new Cell(x);
            cells = rs;
            // 然后设置初始化完成表示为true
            init = true;
        }
    } finally {
        // 重新设置cellsBusy标识,操作完成,没人在初始化或者扩容了
        cellsBusy = 0;
    }
    // 如果初始化完成,退出循环
    if (init)
        break;
}



如果cells备用窗口时null,则初始化一下;同时如果自己被派遣到的窗口格子是空的,则创建一个窗口格子(相当于叫一个工作人员过来)

分支1的源码:备用窗口已经开启,准备去备用窗口:

if ((as = cells) != null && (n = as.length) > 0) {
    // 如果自己被派到的那个备用窗口为null,说明窗口还么人工作,则叫一个工作人员过来负责
    if ((a = as[(n - 1) & h]) == null) {
        // 如果没人在初始化,下面则创建一个窗口(叫一个工作人员过来处理)
        if (cellsBusy == 0) {
            // 创建一个新的窗口,同时提交自己的请求x
            Cell r = new Cell(x);
            // 如果没人在初始化
            if (cellsBusy == 0 && casCellsBusy()) {
                boolean created = false;
                try {
                    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 {
                    cellsBusy = 0;
                }
                if (created)
                    break;
                continue; // Slot is now non-empty
            }
        }
        collide = false;
    }
    // 如果到这里已经知道cas操作失败了,则重新设置一下失败标识,进入下一循环重试
    else if (!wasUncontended) // CAS already known to fail
        wasUncontended = true; // Continue after rehash
    // 根据 用户id % 备用窗口总数 算法,自己应该争抢哪个备用窗口
    // 如果争抢成功,则操作结束了,如果失败进入下一循环重试
    else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                 fn.applyAsLong(v, x))))
        break;
    else if (n >= NCPU || cells != as)
        collide = false;
    else if (!collide)
        collide = true;
    // 走到这里,说明上面的操作都失败了,备用窗口竞争还是很激烈
    // 所以需要扩容了,多增加一些备用窗口,缓解竞争激烈的情况
    else if (cellsBusy == 0 && casCellsBusy()) {
        try {
            // 这里就是具体的扩容操作了
            if (cells == as) { // Expand table unless stale
                // 新的备用窗口是原来的2倍
                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
    }
    // 在这里重新计算一下自己的用户id,
    // 使得用户id尽量的分散到不同的窗口,减少竞争
    h = advanceProbe(h);
}

在这里插入图片描述

<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、付费专栏及课程。

余额充值