最近在阅读 JDK8 并发包中的一些源码,发现很多有趣规律,其中一条就是自旋+CAS实现无锁并发控制。
自旋(for(;;)) + CAS = 自旋锁。for(;;) 也可以换成 while(true) 等形式。
一、自旋(for(;;)) + CAS 实现无锁并发控制
什么是自旋?
自旋是指一个线程在某个条件不满足时,不放弃CPU控制权,而是在该条件下不断循环检查,直到条件满足。
使用自旋循环而不是阻塞线程,可以减少线程上下文切换的开销,特别是在等待时间非常短的情况下,自旋可以提供更好的性能。
自旋和 + CAS(compareAndSwap
) 。非常经典的套路。 ConcurrentHashMap
、AQS
源码,都是相同的分支走向,骨架几乎一致。
案例说明
案例:ConcurrentHashMap#initTable()
这是初始化数组的核心方法。
- 初始化数组方法,但只能进行一次,只有在 CAS 成功后才能完成初始化; CAS成功则类似获得锁,获得操作权限。
- 由于是并发,存在多个线程进入 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 能力,通常代码套路如下:
- 定义 sun.misc.Unsafe 变量,通过 static 初始化
- 定义一个 long 类型的控制变量,使用 Unsafe 工具方法获得其在 ConcurrentHash 的类的偏移量 offSet
- unsafe 使用这个偏移量 offSet 进行 CAS 操作
unsafe 支持 int、long、object,还是很全面的。
CompletableFuture、AbstractQueuedSynchronizer(AQS)、AtomicXX 等,这些高性能的类都用到了这个套路。
注意事项:
- CAS 存在 ABA 问题
- 长时间自旋可能消耗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 实现无锁并发控制。
理解这个套路,对于掌握很多并发包中的源码非常有帮助。
当然这个套路也能运用到日常工作中,实现无锁并发控制。
但需要注意:
- CAS 存在 ABA 问题
- 自旋会持有 CPU,避免大量线程、长时间自旋
- 一定要控制退出条件
本文到此结束,感谢阅读!