JDK高性能套路: 自旋(for(;;)) + CAS

最近在阅读 JDK8 并发包中的一些源码,发现很多有趣规律,其中一条就是自旋+CAS实现无锁并发控制。

自旋(for(;;)) + CAS = 自旋锁。for(;;) 也可以换成 while(true) 等形式。

一、自旋(for(;;)) + CAS 实现无锁并发控制

什么是自旋?

自旋是指一个线程在某个条件不满足时,不放弃CPU控制权,而是在该条件下不断循环检查,直到条件满足。

使用自旋循环而不是阻塞线程,可以减少线程上下文切换的开销,特别是在等待时间非常短的情况下,自旋可以提供更好的性能。

自旋和 + CAS(compareAndSwap) 。非常经典的套路。 ConcurrentHashMapAQS 源码,都是相同的分支走向,骨架几乎一致

案例说明

案例:ConcurrentHashMap#initTable()

这是初始化数组的核心方法。

  1. 初始化数组方法,但只能进行一次,只有在 CAS 成功后才能完成初始化; CAS成功则类似获得锁,获得操作权限。
  2. 由于是并发,存在多个线程进入 initTable 方法, 所以 CAS 可能会失败,因此通过 while()、for 等多次循环保证可以完成初始化,同时又妥善处理了其他进入方法的线程。

相关代码如下:

举个例子: 模拟ABC三个线程,只有 CAS 成功才能初始化 Table,否则自旋,直到满足条件退出。 通过 CAS 实现了并发控制。

上面的代码逻辑,是非常标准的套路。

AQS 中非公平锁在获取独占锁的关键代码也采用了这个套路。具体位置如下:

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

涉及到多线程并发获取锁的情况,都可以考虑采用 CAS 来完成。

CAS 的代码套路

CAS操作主要通过sun.misc.Unsafe类提供的native本地方法实现,JDK底层通过C++函数"Unsafe_CompareAndSetInt"来实现这些方法。在操作系统层面,CAS操作通常由硬件指令实现,比如x86架构下的cmpxchg指令,保证了原子性

CAS 是借助 Unsafe 的能力,实现 CAS 能力,通常代码套路如下:

  1. 定义 sun.misc.Unsafe 变量,通过 static 初始化
  2. 定义一个 long 类型的控制变量,使用 Unsafe 工具方法获得其在 ConcurrentHash 的类的偏移量 offSet
  3. unsafe 使用这个偏移量 offSet 进行 CAS 操作

unsafe 支持 int、long、object,还是很全面的。

CompletableFuture、AbstractQueuedSynchronizer(AQS)、AtomicXX 等,这些高性能的类都用到了这个套路。

注意事项:

  1. CAS 存在 ABA 问题
  2. 长时间自旋可能消耗CPU、一定要控制退出条件

由于Unsafe类提供的方法绕过了 Java 的安全检查,因此它被标记为不推荐使用的内部 API,并且在未来的 Java 版本中可能会发生变化或被移除。 但是我们可以使用它的一些工具类。

比如:AtomicInteger 来实现一些并发控制!下面是 AtomicInteger 的 compareAndSet 的方法:

Java 复制代码
/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

接下来以一个案例来说明 CAS 的并发控制!

二、使用 CAS 实现锁的功能

功能描述:使用ABC三个线程,进行循环打印,使用 AtomicInteger 接口工具类能力:

具体代码如下:

  • 线程A:从0到1,CAS 成功则打印 A。 完成后,设置 2,让 B 线程有机会执行
  • 线程B:从2到3, CAS 成功则打印 B。完成后,设置 4,让 C 线程有机会执行
  • 线程C:从4到5, CAS 成功则打印 C。 完成后,设置 0,让 A 线程有机会执行

通过上面的逻辑,形成循环。

具体代码如下:

由于自旋,并不是让出 CPU,因此会一直持有 CPU。下面是 visual 监控 CPU 和 线程的情况:

三个线程一直持有 CPU

三个线程一直处于 RUNNING 状态

还可以使用其他的AtomicLong、AtomicBoolean 等控制。

在 ConcurrentHashMap 中,通过变量值 SIZECTL 来控制不同的阶段; 其处理逻辑几乎一致!

三、总结

自旋(for(;;)) + CAS 实现无锁并发控制。

理解这个套路,对于掌握很多并发包中的源码非常有帮助。

当然这个套路也能运用到日常工作中,实现无锁并发控制。

但需要注意:

  1. CAS 存在 ABA 问题
  2. 自旋会持有 CPU,避免大量线程、长时间自旋
  3. 一定要控制退出条件

本文到此结束,感谢阅读!

相关推荐
AI进化营-智能译站3 小时前
ROS2 C++开发系列17-多线程驱动多传感器|chrono高精度计时实现机器人同步控制
java·c++·ai·机器人
qq_589568106 小时前
springbootweb案例,出现访问 http://localhost:8080/list 一直处于浏览器运转阶段
java·网络协议·http·list·springboot
JAVA面经实录9177 小时前
计算机基础(完整版·超详细可背诵)
java·linux·数据结构·算法
AC赳赳老秦7 小时前
知识产权辅助:用 OpenClaw 批量生成专利交底书 / 软著申请材料,自动校验格式与内容合规性
java·人工智能·python·算法·elasticsearch·deepseek·openclaw
小码哥_常8 小时前
告别MySQL!大厂集体转投PostgreSQL,到底藏着什么玄机?
后端
FYKJ_20108 小时前
springboot校园兼职平台--附源码02041
java·javascript·spring boot·python·eclipse·django·php
书源丶8 小时前
三十六、File 类与 IO 流基础——文件操作的「第一步」
java
刀法如飞9 小时前
Go数组去重的20种实现方式,AI时代解决问题的不同思路
后端·算法·go
AI人工智能+电脑小能手9 小时前
【大白话说Java面试题】【Java基础篇】第30题:JDK动态代理和CGLIB动态代理有什么区别
java·开发语言·后端·面试·代理模式