从LongAdder 看更高效的无锁实现

本文深入探讨了LongAdder相较于AtomicLong的高效性,分析了LongAdder的设计原理及其实现细节,揭示了它如何通过减少并发压力来提高性能。

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

接触到AtomicLong的原因是在看guava的LoadingCache相关代码时,关于LoadingCache,其实思路也非常简单清晰:用模板模式解决了缓存不命中时获取数据的逻辑,这个思路我早前也正好在项目中使用到。

言归正传,为什么说LongAdder引起了我的注意,原因有二:
1. 作者是Doug lea ,地位实在举足轻重。
2. 他说这个比AtomicLong高效。
我们知道,AtomicLong已经是非常好的解决方案了,涉及并发的地方都是使用CAS操作,在硬件层次上去做 compare and set操作。效率非常高。
因此,我决定研究下,为什么LongAdder比AtomicLong高效。
首先,看LongAdder的继承树:
la1
继承自Striped64,这个类包装了一些很重要的内部类和操作。稍候会看到。
正式开始前,强调下,我们知道,AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过cas 指令从机器指令级别操作保证并发的原子性。
再看看LongAdder的方法:
la2
怪不得可以和AtomicLong作比较,连API都这么像。我们随便挑一个API入手分析,这个API通了,其他API都大同小异,因此,我选择了add这个方法。事实上,其他API也都依赖这个方法。
la3
LongAdder中包含了一个Cell 数组,Cell是Striped64的一个内部类,顾名思义,Cell 代表了一个最小单元,这个单元有什么用,稍候会说道。先看定义:
la4
Cell内部有一个非常重要的value变量,并且提供了一个CAS更新其值的方法。
回到add方法:
la3
这里,我有个疑问,AtomicLong已经使用CAS指令,非常高效了(比起各种锁),LongAdder如果还是用CAS指令更新值,怎么可能比AtomicLong高效了? 何况内部还这么多判断!!!
这是我开始时最大的疑问,所以,我猜想,难道有比CAS指令更高效的方式出现了? 带着这个疑问,继续。
第一if 判断,第一次调用的时候cells数组肯定为null,因此,进入casBase方法:
la5
原子更新base没啥好说的,如果更新成功,本地调用开始返回,否则进入分支内部。
什么时候会更新失败? 没错,并发的时候,好戏开始了,AtomicLong的处理方式是死循环尝试更新,直到成功才返回,而LongAdder则是进入这个分支。
分支内部,通过一个Threadlocal变量threadHashCode 获取一个HashCode对象,该HashCode对象依然是Striped64类的内部类,看定义:
la6
有个code变量,保存了一个非0的随机数随机值。
回到add方法:
la3
拿到该线程相关的HashCode对象后,获取它的code变量,as[(n-1)h] 这句话相当于对h取模,只不过比起取摸,因为是 与 的运算所以效率更高。
计算出一个在Cells 数组中当先线程的HashCode对应的 索引位置,并将该位置的Cell 对象拿出来更新cas 更新它的value值。
当然,如果as 为null 并且更新失败,才会进入retryUpdate方法。
看到这里我想应该有很多人明白为什么LongAdder会比AtomicLong更高效了,没错,唯一会制约AtomicLong高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低。
那怎么解决? LongAdder给了我们一个非常容易想到的解决方案: 减少并发,将单一value的更新压力分担到多个value中去,降低单个value的 “热度”,分段更新!!!
  这样,线程数再多也会分担到多个value上去更新,只需要增加cell就可以降低 value的 “热度”  AtomicLong中的 恶性循环不就解决了吗? cells 就是用来存储这个 “段”的,每个段又叫做cell, cell中的value 就是存放更新值的, 这样,当我需要总数时,把cells 中的value都累加一下不就可以了么!!
当然,聪明之处远远不仅仅这里,在看看add方法中的代码,casBase方法可不可以不要,直接分段更新,上来就计算 索引位置,然后更新value?
答案是不好,不是不行,因为,casBase操作等价于AtomicLong中的cas操作,要知道,LongAdder中分段更新这样的处理方式是有坏处的,分段操作必然带来空间上的浪费,可以空间换时间,但是,能不换就不换,要空间时间都节约~! 所以,casBase操作保证了在低并发时,不会立即进入分支做分段更新操作,因为低并发时,casBase操作基本都会成功,只有并发高到一定程度了,才会进入分支,所以,Doug Lead对该类的说明是: 低并发时LongAdder和AtomicLong性能差不多,高并发时LongAdder更高效!
la7
但是,Doung Lea 还是没这么简单,聪明之处还没有结束……
虽然如此,retryUpdate中做了什么事,也基本略知一二了,因为cell中的value都更新失败(说明该索引到这个cell的线程也很多,并发也很高时) 或者cells数组为空时才会调用retryUpdate,
因此,retryUpdate里面应该会做两件事:
1. 扩容,将cells数组扩大,降低每个cell的并发量,同样,这也意味着cells数组的rehash动作。
2. 给空的cells变量赋一个新的Cell数组。
是不是这样呢? 继续看代码:
代码比较长,变成文本看看,为了方便大家看if else 分支,对应的  { } 我在括号后面用分支XXX 标注出来
可以看到,这个时候Doug Lea才愿意使用死循环保证更新成功~!
因为进入这个方法的几率已经非常低了。
final void retryUpdate( long x, HashCode hc, boolean wasUncontended) {
       int h = hc.code;
       boolean collide = false ;                // True if last slot nonempty
       for (;;) {
           Cell[] as; Cell a; int n; long v;
           if ((as = cells) != null && (n = as.length) > 0 ) { // 分支1
               if ((a = as[(n - 1 ) & h]) == null ) {
                   if (busy == 0 ) {            // Try to attach new Cell
                       Cell r = new Cell(x);   // Optimistically create
                       if (busy == 0 && casBusy()) {
                           boolean created = false ;
                           try {               // Recheck under lock
                               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 {
                               busy = 0 ;
                           }
                           if (created)
                               break ;
                           continue ;           // Slot is now non-empty
                       }
                   }
                   collide = false ;
               }
               else if (!wasUncontended)       // CAS already known to fail
                   wasUncontended = true ;      // Continue after rehash
               else if (a.cas(v = a.value, fn(v, x)))
                   break ;
               else if (n >= NCPU || cells != as)
                   collide = false ;            // At max size or stale
               else if (!collide)
                   collide = true ;
               else if (busy == 0 && casBusy()) {
                   try {
                       if (cells == as) {      // Expand table unless stale
                           Cell[] rs = new Cell[n << 1 ];
                           for ( int i = 0 ; i < n; ++i)
                               rs[i] = as[i];
                           cells = rs;
                       }
                   } finally {
                       busy = 0 ;
                   }
                   collide = false ;
                   continue ;                   // Retry with expanded table
               }
               h ^= h << 13 ;                   // Rehash                 h ^= h >>> 17;
               h ^= h << 5 ;
           }
           else if (busy == 0 && cells == as && casBusy()) { //分支2
               boolean init = false ;
               try {                           // Initialize table
                   if (cells == as) {
                       Cell[] rs = new Cell[ 2 ];
                       rs[h & 1 ] = new Cell(x);
                       cells = rs;
                       init = true ;
                   }
               } finally {
                   busy = 0 ;
               }
               if (init)
                   break ;
           }
           else if (casBase(v = base, fn(v, x)))
               break ;                          // Fall back on using base
       }
       hc.code = h;                            // Record index for next time
   }
分支2中,为cells为空的情况,需要new 一个Cell数组。
分支1分支中,略复杂一点点:
注意,几个分支中都提到了busy这个方法,这个可以理解为一个CAS实现的锁,只有在需要更新cells数组的时候才会更新该值为1,如果更新失败,则说明当前有线程在更新cells数组,当前线程需要等待。重试。
回到分支1中,这里首先判断当前cells数组中的索引位置的cell元素是否为空,如果为空,则添加一个cell到数组中。
否则更新 标示冲突的标志位wasUncontended 为 true ,重试。
否则,再次更新cell中的value,如果失败,重试。
。。。。。。。一系列的判断后 ,如果还是失败,下下下策,reHash,直接将cells数组扩容一倍,并更新当前线程的hash值,保证下次更新能尽可能成功。
可以看到,LongAdder确实用了很多心思减少并发量,并且,每一步都是在”没有更好的办法“的时候才会选择更大开销的操作,从而尽可能的用最最简单的办法去完成操作。追求简单,但是绝对不粗暴。

————————————————分割线——————————————————————-

昨天在coolshell    投稿后 (文章在这里) 和左耳朵耗子简单讨论了下,发现左耳朵耗子对读者思维的引导还是非常不错的,在第一次发现这个类后,对里面的实现又提出了更多的问题,引导大家思考,值得学习,赞一个~

我们 发现的问题有这么几个:

1. jdk 1.7中是不是有这个类?
我确认后,结果如下:    jdk-7u51版本上 上还没有  但是jdk-8u20 版本上已经有了。代码基本一样 ,增加了对double类型的支持和删除了一些冗余的代码。

2. base有没有参与汇总?
base在调用intValue等方法的时候是会汇总的:

LA10
3. base的顺序可不可以调换?
左耳朵耗子,提出了这么一个问题: 在add方法中,如果cells不会为空后,casBase方法一直都没有用了?

因此,我想可不可以调换add方法中的判断顺序,比如,先做casBase的判断,结果是 不调换可能更好,调换后每次都要CAS一下,在高并发时,失败几率非常高,并且是恶性循环,比起一次判断,后者的开销明显小很多,还没有副作用。因此,不调换可能会更好。

4. AtomicLong可不可以废掉?
我的想法是可以废掉了,因为,虽然LongAdder在空间上占用略大,但是,它的性能已经足以说明一切了,无论是从节约空的角度还是执行效率上,AtomicLong基本没有优势了,具体看这个测试(感谢coolshell读者Lemon的回复):http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/

<think>嗯,用户问为什么在线程竞争激烈的时候,使用Lock(比如ReentrantLock)的性能比synchronized好。之前已经讨论过重量级,现在需要深入比较两者的机制。 首先,我需要回顾synchronized在竞争激烈时的表现。当升级到重量级后,它依赖操作系统的互斥量,导致线程挂起和唤醒需要切换到内核态,这会带来较大的开销,尤其是在高并发时频繁切换,性能下降明显。 而ReentrantLock是基于AQS实现的,使用CAS和CLH队列管理线程。CAS是用户态的操作,避免了内核切换。此外,ReentrantLock提供了可中断的获取、超时机制和公平选项,这些功能在竞争激烈时能灵活地控制线程行为,减少不必要的阻塞。 用户可能的深层需求是优化高并发程序的性能。他们可能遇到了synchronized在高竞争下的性能问题,想寻找替代方案。需要解释两者的底层机制差异,特别是为什么Lock在激烈竞争时高效,比如减少上下文切换、自旋等待和细粒度的控制。 另外,要提到具体的优化策略,比如减少的持有时间、使用读写等,帮助用户实际应用。可能还需要补充测试数据或场景举例,比如在大量线程竞争时,Lock的表现优于synchronized,但低竞争时可能相反。 要注意避免技术错误,比如明确synchronized在JDK版本中的优化,但高竞争下仍不敌Lock。还要对比两者的使用复杂度,Lock需要手动释放,但提供了多功能。最后总结适用场景,帮助用户根据具体情况选择。</think>在 **线程竞争激烈** 的场景下,`ReentrantLock`(即 `Lock` 接口的典型实现)的性能通常优于 `synchronized`,主要原因在于两者的 **实现机制** 和 **竞争策略** 不同。以下是具体分析: --- ### **1. 底层实现差异** | **特性** | `synchronized`(重量级) | `ReentrantLock`(`Lock` 实现类) | |------------------------|----------| | **底层依赖** | 依赖操作系统 **互斥量(Mutex)**,需内核态切换。 | 基于 **AQS(AbstractQueuedSynchronizer)**,通过 CAS 和队列管理,全程用户态操作。 | | **线程阻塞方式** | 竞争失败时,线程直接挂起(阻塞),依赖内核调度。 | 先尝试 CAS 获取,失败后通过 CLH 队列管理线程,支持自旋等待减少阻塞。 | | **竞争策略** | 被动升级为重量级,无法灵活控制自旋。 | 可自定义自旋次数、超时机制,甚至支持公平。 | --- ### **2. 性能优势的核心原因** #### **(1) 减少内核态切换开销** - **`synchronized` 的重量级**: 竞争失败时,线程会直接被操作系统挂起(从用户态切换到内核态),释放后需要再切换回用户态唤醒线程。**频繁的上下文切换** 在竞争激烈时成为性能瓶颈。 - **`ReentrantLock`**: 通过 AQS 的 **CLH 队列** 管理竞争线程,优先通过 **CAS(用户态操作)** 尝试获取。即使失败,线程会先在队列中自旋等待,**减少直接挂起的概率**,从而减少内核态切换次数。 #### **(2) 细粒度的控制** - **可中断**: `ReentrantLock` 支持 `lockInterruptibly()`,允许线程在等待时响应中断,避免死。 - **超时机制**: 支持 `tryLock(timeout)`,若未在指定时间内获取,线程可主动放弃,避免无限等待。 - **公平选项**: 可配置为公平(按等待顺序分配),减少线程饥饿问题,在高竞争下稳定。 #### **(3) 自旋优化** - **`synchronized`**: 轻量级阶段的 **自旋次数由 JVM 控制**(自适应自旋),升级为重量级后不再自旋,直接挂起线程。 - **`ReentrantLock`**: 在竞争激烈时,AQS 的 **CLH 队列机制允许线程在队首短暂自旋**,避免立即挂起,降低上下文切换频率。 -- ### **3. 实际场景对比** #### **高并发竞争下的性能测试** 在大量线程(如 100+)频繁争抢同一把时: - **`synchronized`**: 大量线程频繁挂起和唤醒,导致 **CPU 时间片浪费在内核态切换**,吞吐量显著下降。 - **`ReentrantLock`**: - 通过 CAS 和队列管理,减少线程阻塞。 - 即使线程需要等待,也能通过自旋或超时机制避免完全挂起。 - 吞吐量通常比 `synchronized` 高 **20%~50%**(具体取决于竞争强度)。 --- ### **4. 代码示例对比** #### **`synchronized` 在高竞争下的表现** ```java public class SynchronizedExample { private int counter = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { // 高竞争时升级为重量级 counter++; } } } ``` - 问题:线程竞争激烈时,频繁的内核态切换导致性能骤降。 #### **`ReentrantLock` 在高竞争下的优化** ```java public class ReentrantLockExample { private int counter = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 通过 AQS 的 CAS 和队列管理竞争 try { counter++; } finally { lock.unlock(); } } } ``` - 优势:通过用户态的 CAS 和队列调度,减少内核态切换。 --- ### **5. 适用场景总结** | **场景** | `synchronized` 适用性 | `ReentrantLock` 适用性 | |------------------------|-----| | **低竞争**(单线程/少量线程) | ✅ 性能好(无升级开销) | ⚠️ 代码复杂,可能不如 `synchronized` 高效 | | **高竞争**(大量线程频繁争抢) | ❌ 性能差(内核切换频繁) | ✅ 性能优(用户态自旋 + 队列调度) | | **需要高级功能**(超时、公平) | ❌ 不支持 | ✅ 原生支持 | --- ### **6. 进一步优化建议** 1. **减少粒度**: - 使用分段(如 `ConcurrentHashMap`)或细粒度,分散竞争。 2. **无编程**: - 使用 `Atomic` 类(如 `AtomicInteger`)或 `LongAdder` 替代。 3. **读写分离**: - 高并发读场景使用 `ReadWriteLock`(如 `ReentrantReadWriteLock`)。 --- ### **7. 总结** - **高竞争下 `Lock` 性能好**: 核心原因是 `ReentrantLock` 通过 **用户态的 CAS 和队列调度**,减少内核态切换,同时支持灵活的竞争策略(如自旋、超时、公平)。 - **`synchronized` 的劣势**: 重量级依赖内核态操作,无法避免频繁的上下文切换,导致吞吐量下降。 在实际开发中,应根据 **竞争强度** 和 **功能需求** 选择机制。高竞争场景下,优先考虑 `ReentrantLock`;低竞争场景下,`synchronized` 的简洁性可能是好的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值