3 张图带你看透 LongAdder

 关注公众号【1024个为什么】,及时接收最新推送文章! 

本文内容:

1、AtomicLong 解决并发的方案?

2、AtomicLong 的不足是什么?

3、LongAdder 又是怎么弥补这一不足的?

在我的过往经历中,用到原子类的地方不多,就算用到了,AtomicInteger 也能满足业务场景,其它几个原子类从来没用过。

为什么研究 AtomicLong 和 LongAdder ?

一是前段时间组内一起学习并发相关知识的过程中涉及到了原子类;

二是我也好奇大神解决高并发的思路。

||  AtomicLong 解决并发的方案

有关 AtomicLong 的基本信息,本文不再赘述。直接通过一个核心方法,看看其内部是如何实现的。

| 核心方法

我们只看自增这一个方法:

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

可以看到,内部调用的是 Unsafe 类的 getAndAddLong 方法,继续看这个方法内的逻辑:

public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, v + delta));
    return v;
}

不难发现,核心操作就是 while 条件里的 compareAndSwapLong()。

也就是我们常说的 CAS。

所以 AtomicLong 解决并发的核心思想就是CAS + 自旋。

||  AtomicLong 的不足是什么

| 高并发下效率低

没有那种解决方案是万能的,AtomicLong 也有它的局限性。

从源码不难看出,在高并发下 CAS 会大量失败,而且在while 循环里,会一直占用CPU资源。

如果每次自增都要经过很多次 CAS 失败,效率自然很低。

||  LongAdder 又是怎么弥补这一不足的

| 分而治之

思想很简单,都往一个变量上累加,效率不仅低,还会随着并发增加而更低。

那能不能多整几个变量,假设提供10个变量(a0 ... a9),根据线程ID末位数字,找到与其对应的变量进行累加。

这样一来,理论上效率至少能提高9倍。

当然这里只是举个例子,Doug Lea 考虑的场景要复杂的多,接下来就通过一系列的图,来阐述LongAdder的设计思想。

先来看一下 LongAdder 的类结构,其核心逻辑主要都在父类 Striped64 的 longAccumulate() 方法中,还要重点关注父类的3个变量(红框已标注)。

934af87eaf1d74ddf8ce0248fcc3ab4a.png

正餐开始之前先做个简单说明,我们把并发的级别先分为3个等级,1,2,3,并发越来越高。

假设我们的代码是在 4 核 CPU 的机器上运行,执行的是自增方法。

| 一级并发场景

和 AtomicLong 等效。在 base 这个变量上 CAS 自增就可以。

2fddea805225a9ba7eb93a572bbd1801.png

此时两个线程竞争的只是在 base 值上 CAS 操作自增,对应源码的逻辑是这里:

35273434d46b567ccf1d3bc780e3f9c4.png

CAS 成功,这次自增操作就会结束。

| 二级并发场景(实例化 cells )

dbcbc5b7118a122a595bfc5e246f0814.png

此时同时来了 3 个线程,thread-1 对 base CAS 累加成功,thread-2、thread-3 失败,也就是图中第一层表示的含义。

thread-2、thread-3 都会进入 Striped64#longAccumulate() 方法中,也就是图中的第二层。两个线程都要往 cells  中累加,此时的 cells 还是 null 。

两个线程要先对 cells 实例化后才能放值,实例化前又要对 cellBusy 加锁 ,否则 2 个线程都实例化有相互覆盖的可能。

假设 thread-2 对 cellBusy 加锁成功,thread-3 失败,我们看一下 thread-2 加锁成功之后的操作。

b1851aa92cb954216b8ccc9733d8e0e6.png

从源码中可以看到,是直接实例化了 cells,并把当前要累加的值包装成一个 cell,直接放入 cells 中,然后释放锁,thread-2 的这次累加结束。

我们继续看 thread-3 失败后又去做什么了?

2353fe59d42c3f862a12fef4aded6ed3.png

thread-3 竟然走了回头路,继续对 base CAS 累加操作。

不得不佩服作者的思路:既然你们都去竞争 cells 了,那么 base 的竞争会不会小一些?不防再试一次 base 。假设 caseBase 成功,thread-3 本次累加操作结束,对应图中第二层最右侧。

如果这里还不成功,thread-3 就会自旋进入下一次循环,重新竞争 cells 。

继续看图中第三层,这一层描述的场景是, cells 已经实例化,同时来了2个线程,thread-1 casBase 成功,本次累加结束。thread-2 失败,转而去操作 cells,cells 上无竞争,直接找到位置放入,thread-2 本次累加结束。对应源码如下:

782a291894e2adb20e53dd25b1bb377f.png

| 三级并发场景(cells 扩容)

cde84b3082cfa3782c57ee4d0823787b.png

这张图有点复杂,我们一步步拆解,重点看一下非常倒霉的 thread-3,先看图上第一层的逻辑。

假设 thread-2、thread-3 casBase 失败后都来竞争 cells[1],thread-2 竞争成功,cells[1]++,本次累加结束。

thread-3 竞争 cells[1] 失败后,且 cells 的容量不大于 CPU 核数,会执行到下面的代码:

de958e745936c6a6a9525dbffd7247a3.png

这里只会把扩容标记置为 true,最下面 rehash 之后会进入下一次循环。

看图中的第二层,thread-3 rehash 后被分配到了 cells[0],碰巧 thread-5 又来和它竞争,还没竞争过人家,对应图中的 ① 。

悲催的 thread-3 只能继续往下走,这时发现上一次竞争失败后已经把扩容标记置为 true 了,于是就开启了扩容之旅。

扩容要先对 cellBusy 加锁(任何 cell 操作之前,都要先加锁),好在这次直接加锁成功,对应图中的 ② 。下面就可以执行扩容的逻辑了,就是图中的 ③ ,扩容完之后释放锁,直接进入下一次循环竞争。对应源码如下:

30b158ba41d7359ec73b58ce494ad008.png

继续看图中的第三层,由于扩容之后,thread-3 没有 rehash ,所以下一次循环竞争的还是 cells[0],这次就照顾一下它,不安排其他线程捣乱了,对 cell[0] CAS 累加成功,thread-3 本次累加结束。

| 分久必合

分开累加的确提升了性能,但获取当前值的时候就要麻烦些,每次都需要把 base 和 cells 里的值累加到一起,而且得到的只是一个快照值。

再看一下源码,为了追求极致性能,累加时都没有加锁。

0854f74067530f2a93aa280e22487668.png

好了,整个过程到此结束。

扯两句

分而治之的方法论,很多场景都适用,一旦拆分,带来的问题也是指数级的

要抓住核心问题去解决,其他次要问题可以适当取舍

留个思考为什么扩容之后没有像初始化时那样,直接再 rehash 一次把值放到 cells 里,而是继续下一次循环呢?

欢迎大家留言讨论!

原创不易,如有收获,一键三连,感谢支持!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Docker是一种流行的容器化技术,通过轻量级、隔离性强的容器来运行应用程序。下面我将通过十张图你深入理解Docker容器和镜像。 1. 第一张图展示了Docker容器和镜像的关系。镜像是Docker的基础组件,它是一个只读的模板,包含了运行应用程序所需的所有文件和配置。容器是从镜像创建的实例,它具有自己的文件系统、网络和进程空间。 2. 第二张图展示了Docker容器的隔离性。每个容器都有自己的文件系统,这意味着容器之间的文件互不干扰。此外,每个容器还有自己的网络和进程空间,使得容器之间的网络和进程相互隔离。 3. 第三张图展示了Docker镜像和容器的可移植性。镜像可以在不同的主机上运行,只需在目标主机上安装Docker引擎即可。容器也可以很容易地在不同的主机上迁移,只需将镜像传输到目标主机并在其上创建容器。 4. 第四张图展示了Docker容器的快速启动。由于Docker容器与主机共享操作系统内核,启动容器只需几秒钟的时间。这使得快速部署和扩展应用程序成为可能。 5. 第五张图展示了Docker容器的可重复性。通过使用Dockerfile定义镜像构建规则,可以确保每次构建的镜像都是相同的。这样,可以消除由于环境差异导致的应用程序运行问题。 6. 第六张图展示了Docker容器的资源隔离性。Docker引擎可以为每个容器分配一定数量的CPU、内存和磁盘空间,确保容器之间的资源不会互相干扰。 7. 第七张图展示了Docker容器的可扩展性。通过使用Docker Swarm或Kubernetes等容器编排工具,可以在多个主机上运行和管理大规模的容器群集。 8. 第八张图展示了Docker镜像的分层结构。镜像由多个只读层组成,每个层都包含一个或多个文件。这种分层结构使得镜像的存储和传输变得高效。 9. 第九张图展示了Docker容器的生命周期。容器可以通过创建、启动、停止和销毁等命令来管理。这使得容器的维护和管理变得简单。 10. 第十张图展示了Docker容器的应用场景。Docker容器广泛应用于开发、测试、部署和运维等领域。它可以提供一致的开发和运行环境,简化了应用程序的管理和交付过程。 通过这十张图,希望能让大家更深入地理解Docker容器和镜像的概念、特性和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值