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

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

相关推荐
Json_1 分钟前
学习springBoot框架-开发一个酒店管理系统,熟悉springboot框架语法~
java·spring boot·后端
用户68545375977694 分钟前
⚡ ZGC:Java界的"闪电侠"!但是...这些坑你得注意!🕳️
后端
用户68545375977694 分钟前
🏦 TLAB:每个线程的专属小金库,对象分配So Easy!
后端
Yimin5 分钟前
1. 了解 系统调用 与 C标准库
后端
用户68545375977695 分钟前
🔍 CPU不高但响应慢:性能排查的福尔摩斯式推理!
后端
用户904706683576 分钟前
java hutool 工具库
后端
kkkkk0211069 分钟前
微服务学习笔记(黑马商城)
java·spring boot·spring·spring cloud·sentinel·mybatis·java-rabbitmq
鄃鳕9 分钟前
Flask【python】
后端·python·flask
2503_9301239310 分钟前
Kubernetes (六)调度策略详解:从节点匹配到Pod调度全流程
java·开发语言
渣哥22 分钟前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试