本文目录导读:
这是一个非常经典且务实的并发编程问题,CAS(Compare-And-Swap,比较并交换)自旋等待是一种轻量级的同步策略,但在高竞争下,无休止的自旋会浪费 CPU 资源。
优化自旋次数的核心思想是:在“等待成功的收益”和“自旋消耗的CPU”之间找到平衡点。
下面从几个层面来拆解优化策略,从最简单到最复杂。
核心原则:使用自适应自旋
这是最系统、最主流的优化方式。不要使用固定次数(如100次)的自旋,而应该根据前一次自旋等待的成功情况,动态调整下一次的自旋次数。
- 原理:如果某个锁的持有者刚刚释放锁,那么当前线程自旋成功的概率很高,应增加自旋次数(甚至让出CPU),如果多次自旋都失败,说明锁的持有者可能在做长时间操作(如I/O),应减少自旋次数,直接阻塞线程。
- 实现:JVM(Java虚拟机)中的
synchronized在偏向锁升级到轻量级锁的过程中,就采用了自适应自旋,JVM会记录上一次自旋成功与否的信息,并根据当前线程的状态(比如是否持有其他锁)来动态决定本次自旋次数。 - 看似“放弃”,实为优化:
Thread.yield()或Thread.onSpinWait()(Java 9+,对应硬件指令PAUSE)可以作为自适应自旋的一部分,在自旋消耗过大时主动让出CPU时间片或给处理器一个“Hint(提示)”,告诉它正在自旋,利于硬件层面的优化(如减少功耗、避免内存顺序违规)。
在算法/数据结构层面优化
如果你的CAS操作是在自旋等待一个共享变量达到某个状态(例如无锁队列的队尾指针),可以通过改变数据结构来减少CAS冲突。
-
使用Backoff策略(指数退避):当CAS失败时,不要立即重试,增加一个很小的随机延迟,然后再重试,延迟时间可以指数增长(如 1ms, 2ms, 4ms...),直到达到一个上限。
- 优点:有效错开高并发下的冲突,显著降低总线流量。
- 缺点:增加了平均等待时间,适合对吞吐量要求不高但对冲突敏感的场景。
-
分段CAS(削弱单一热点):将单点竞争的“中心变量”分散到多个槽位上。
- 经典案例:
LongAdder替代AtomicLong,高并发下,所有线程都去更新一个AtomicLong会导致严重的CPU缓存行失效(False Sharing,伪共享)。LongAdder内部有一个cells数组,线程只更新自己哈希到的cell,最后再汇总,这样几乎消除了CAS冲突。
- 经典案例:
-
消除伪共享(False Sharing):通过填充缓存行(如使用
@Contended注解或手动填充变量间隔,使其不在同一个缓存行),避免多个CPU核心上的CAS操作互相无效化缓存,从而减少自旋重试。
结合阻塞机制
这是最现实的优化:不要指望纯自旋解决一切问题。
- 有限次自旋 + 阻塞:先尝试自旋N次(比如10次),如果自旋失败,则放弃CPU,线程进入阻塞等待(如
LockSupport.park()进入休眠队列)。- 优点:结合了自旋的低延迟和阻塞的CPU节省特性。
- 适用:绝大部分需要等待的场景。
- Java实现:
ReentrantLock的内部实现AbstractQueuedSynchronizer(AQS)就是这样的策略,在尝试获取锁时,先进行快速的CAS尝试,失败后才进入等待队列,并在队列中可能再次尝试自旋。
在编程语言/硬件层面的终极优化
这是你跟底层“商量”如何少转圈。
-
使用
Thread.onSpinWait()(Java 9+):- 在自旋循环体里调用这个方法,这个指令会提示CPU:我正在忙等,CPU收到这个提示后,可以采取行动,比如降低执行单元的电压、减少功耗,或者优化流水线以避免因为内存顺序错误而导致的性能惩罚(这对x86的
PAUSE指令尤其重要)。 - 例子:
while (!locked.compareAndSet(false, true)) { Thread.onSpinWait(); // 告诉CPU我在自旋,请优化 }
- 在自旋循环体里调用这个方法,这个指令会提示CPU:我正在忙等,CPU收到这个提示后,可以采取行动,比如降低执行单元的电压、减少功耗,或者优化流水线以避免因为内存顺序错误而导致的性能惩罚(这对x86的
-
使用硬件提供的指令:
- x86的
PAUSE:上面提到的Thread.onSpinWait()在x86上就映射为PAUSE指令,它告诉CPU这不是一个内存屏障,不需要因推测执行而冲刷流水线,同时也能节约功耗。如果没有PAUSE,复杂的推测执行逻辑会因为CAS失败而频繁Rollback,导致性能下降一个数量级。 - ARM的
YIELD/WFE:ARM架构下有类似的指令。
- x86的
一个科学的优化策略
假设你正在设计一个需要自旋等待的系统,一个综合的优化策略应该像下面这样分层:
-
第一步:消除冲突(效果最好)
- 检查能否使用无锁数据结构,或者对数据进行分段(如
LongAdder)。 - 确保数据对齐,避免伪共享。
- 检查能否使用无锁数据结构,或者对数据进行分段(如
-
第二步:采用自适应自旋 + Backoff策略
- 维护一个自旋计数器(如
spinCount)。 - 在循环中执行CAS。
- 失败后,调用
Thread.onSpinWait()给硬件提示。 - 如果连续失败次数超过某个动态阈值(由代码根据历史成功率调整),则执行
Thread.yield()或LockSupport.parkNanos(smallDelay)进行指数退避。
- 维护一个自旋计数器(如
-
第三步:兜底机制(最核心)
- 设定一个最大自旋次数(如 1000次 或一个动态值)。
- 如果达到阈值后CAS仍未成功,立即放弃自旋,改用阻塞(如
LockSupport.park(),进入队列等待)或者直接返回失败。
最终答案: 优化CAS自旋次数的最好方法,是不让它长时间自旋,通过“有限次自适应自旋 + 指数退避 + 最终阻塞/失败”的组合,以及从数据结构层面消除热点,才能做到既快又省。
标签: 自适应自旋