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. 一定要控制退出条件

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

相关推荐
vx-程序开发1 分钟前
【计算机毕设】django校园跑腿服务系统83141
java·数据库·vue.js·spring boot·spring·elasticsearch·django
谁在黄金彼岸4 分钟前
Ollama-使用速查
后端
ServBay5 分钟前
Jev是什么?哑巴模型居然全网爆火
后端·aigc·ai编程
勇气要爆发19 分钟前
006.注解
java·注解
烈风逍遥20 分钟前
第六篇:RAG 知识库构建与检索全链路
前端·人工智能·后端
花椒技术21 分钟前
Agent 沙箱怎么接入生产?花椒的选型、持久化与执行协议实践
人工智能·后端·agent
liuchangng27 分钟前
Jev 模型研究:从生成式大模型到决策式模型——System One、RLCD 校准与采用边界
java·javascript·人工智能·python·深度学习
GPU实战笔记28 分钟前
本地跑不动 llama.cpp,临时租云 GPU 怎么搭?从启动服务到环境复用
java·服务器·网络·人工智能·深度学习·llama
独泪了无痕33 分钟前
Hutool之ArrayUtil:解锁数组操作的新境界
后端
撸串-研究生34 分钟前
SSM + Vue 线上作业批改系统:在线答题、文件提交与教师批改闭环90608
java·ssm·在线答题·作业批改·文件提交