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

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

相关推荐
装不满的克莱因瓶30 分钟前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗37 分钟前
常用类晨考day15
java
骇客野人41 分钟前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro2 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干2 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion2 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob3 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言