CAS 自旋 volatile 变量,是一种很经典的用法,在 java.util.concurrent 包中随处可见,具体原因将在这篇文章中去进行介绍。
1、volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
存在问题:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
2、CAS
CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
这一系列的操作是原子的,CAS 通过调用 JNI (Java Native Interface)调用实现的。JNI允许 Java调用其他语言,而 CAS 就是借助 C 语言来调用 CPU 底层指令实现的。UnSafe 是 CAS 的核心类,它提供了硬件级别的原子操作。
3、CAS + volatile
volatile 保证共享变量的可见性,CAS 保证更新操作的原子性,把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。如果仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
-
首先,声明共享变量为 volatile;
-
然后,使用 CAS 的原子条件更新来实现线程之间的同步;
-
同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
为什么无锁效率高?
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
4、源码分析
以AtomicInteger 类的 getAndIncrement()方法源码为例
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long valueOffset; //内存偏移量
private static final Unsafe unsafe = Unsafe.getUnsafe(); //给Unsafe类的初始化,方便方法中调用。
static {
try {
//给valueOffset初始化
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
private volatile int value;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
valueOffset 是什么?
valueOffset 所代表的是AtomicInteger对象的value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存当前值。也就是说,valueOffset 是当前AtomicInteger对象初始化时的原始值的内存地址。例如:AtomicInteger atomicInteger = new AtomicInteger(5); 这个5就是原始值,即valueOffset 是5的内存地址。
CAS + 自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 由于 value 声明为 volatile,所以以这种方式读取
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
// 在对象指定偏移地址处 volatile 读取一个int
public native int getIntVolatile(Object var1, long var2);
结论:AtomicInteger 通过 CAS + 自旋 + volatile value 保证自增方法成功执行。
5、总结
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的,理解这种用法对于源码的阅读会有很好的帮助。
参考文章:Java CAS(compare and swap)自旋操作(JUC基石--CAS+volatile实现线程通信) - 忙碌了一整天的L师傅 - 博客园 (cnblogs.com)
(2条消息) 多线程教程(二十四)CAS+volatile_今天成为大神了吗的博客-CSDN博客_cas,volatile在cpp文件的实现