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

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

相关推荐
葫芦和十三3 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp4 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑4 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯5 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan7 小时前
多Agent之间的区别
后端
青石路9 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充9 小时前
1.面向对象设计思想
后端
IT_陈寒10 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro10 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗10 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端