Java8中的LongAdder类,提升CAS性能

这篇文章给大家聊一下java并发包下的CAS相关的原子操作,以及Java 8如何改进和优化CAS操作的性能。

因为Atomic系列的原子类,无论在并发编程、JDK源码、还是各种开源项目中,都经常用到。而且在Java并发面试中,这一块也属于比较高频的考点,所以还是值得给大家聊一聊。

场景引入,问题凸现

好,我们正式开始!假设多个线程需要对一个变量不停的累加1,比如说下面这段代码:

实际上,上面那段代码是不ok的,因为多个线程直接这样并发的对一个data变量进行修改,是线程不安全性的行为,会导致data值的变化不遵照预期的值来改变。

举个例子,比如说20个线程分别对data执行一次data++操作,我们以为最后data的值会变成20,其实不是。

最后可能data的值是18,或者是19,都有可能,因为多线程并发操作下,就是会有这种安全问题,导致数据结果不准确。

至于为什么会不准确?那不在本文讨论的范围里,因为这个一般只要是学过java的同学,肯定都了解过多线程并发问题。

初步的解决方案:synchronized

所以,对于上面的代码,一般我们会改造一下,让他通过加锁的方式变成线程安全的:

这个时候,代码就是线程安全的了,因为我们加了synchronized,也就是让每个线程要进入increment()方法之前先得尝试加锁,同一时间只有一个线程能加锁,其他线程需要等待锁。

通过这样处理,就可以保证换个data每次都会累加1,不会出现数据错乱的问题。

老规矩!我们来看看下面的图,感受一下synchronized加锁下的效果和氛围,相当于N个线程一个一个的排队在更新那个数值。

但是,如此简单的data++操作,都要加一个重磅的synchronized锁来解决多线程并发问题,就有点杀鸡用牛刀,大材小用了。

虽然随着Java版本更新,也对synchronized做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。人家synchronized是可以解决更加复杂的并发编程场景和问题的。

而且,在这个场景下,你要是用synchronized,不就相当于让各个线程串行化了么?一个接一个的排队,加锁,处理数据,释放锁,下一个再进来。

更高效的方案:Atomic原子类及其底层原理

对于这种简单的data++类的操作,其实我们完全可以换一种做法,java并发包下面提供了一系列的Atomic原子类,比如说AtomicInteger。

他可以保证多线程并发安全的情况下,高性能的并发更新一个数值。我们来看下面的代码:

大家看上面的代码,是不是很简单!多个线程可以并发的执行AtomicInteger的incrementAndGet()方法,意思就是给我把data的值累加1,接着返回累加后最新的值。

这个代码里,就没有看到加锁和释放锁这一说了吧!

实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性

那什么是CAS呢?他的全称是:Compare and Set,也就是先比较再设置的意思。

话不多说,先上图!

我们来看上面的图,假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:

首先,每个线程都会先获取当前的值,接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。

然后CAS操作里,会比较一下说,唉!大兄弟!现在你的值是不是刚才我获取到的那个值啊?

如果是的话,bingo!说明没人改过这个值,那你给我设置成累加1之后的一个值好了!

同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作!

好!现在我们对照着上面的图,来看一下这整个过程:

  • 首先第一步,我们假设线程一咔嚓一下过来了,然后对AtomicInteger执行incrementAndGet()操作,他底层就会先获取AtomicInteger当前的值,这个值就是0。
  • 此时没有别的线程跟他抢!他也不管那么多,直接执行原子的CAS操作,问问人家说:兄弟,你现在值还是0吗?
  • 如果是,说明没人修改过啊!太好了,给我累加1,设置为1。于是AtomicInteger的值变为1!
  • 接着线程2和线程3同时跑了过来,因为底层不是基于锁机制,都是无锁化的CAS机制,所以他们俩可能会并发的同时执行incrementAndGet()操作。
  • 然后俩人都获取到了当前AtomicInteger的值,就是1
  • 接着线程2抢先一步发起了原子的CAS操作!注意,CAS是原子的,此时就他一个线程在执行!
  • 然后线程2问:兄弟,你现在值还是1吗?如果是,太好了,说明没人改过,我来改成2

好了,此时AtomicInteger的值变为了2。关键点来了: 现在线程3接着发起了CAS操作,但是他手上还是拿着之前获取到的那个1啊!

线程3此时会问问说:兄弟,你现在值还是1吗?

噩耗传来!!! 这个时候的值是2啊!线程3哭泣了,他说,居然有人在这个期间改过值。算了,那我还是重新再获取一次值吧,于是获取到了最新的值,值为2。

然后再次发起CAS操作,问问,现在值是2吗?是的!太好了,没人改,我抓紧改,此时AtomicInteger值变为3!

上述整个过程,就是所谓Atomic原子类的原理,没有基于加锁机制串行化,而是基于CAS机制:先获取一个值,然后发起CAS,比较这个值被人改过没?如果没有,就更改值!这个CAS是原子的,别人不会打断你!

通过这个机制,不需要加锁这么重量级的机制,也可以用轻量级的方式实现多个线程安全的并发的修改某个数值。

Java 8对CAS机制的优化

但是这个CAS有没有问题呢?肯定是有的。比如说大量的线程同时并发修改一个AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起CAS操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环。

在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。

于是,当当当当,Java 8推出了一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!

在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,比如刚开始累加成了base = 5。

接着如果发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。

这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!

这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!

而且他内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。

这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。

最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。

<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]。 --- ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值