JVM 源码分析(三):深入理解 CAS
张永恒 人气:0- 前言
- 什么是 CAS
- Java 中的 CAS
- JVM 中的 CAS
前言
在上一篇文章中,我们完成了源码的编译和调试环境的搭建。
鉴于 CAS 的实现原理比较简单, 然而很多人对它不够了解,所以本篇将从 CAS 入手,首先介绍它的使用,然后分析它在 Hotsport 虚拟机中的具体实现。
什么是 CAS
CAS(Compare And Swap,比较并交换)通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。
CAS 的逻辑用伪代码描述如下:
if (value == expectedValue) {
value = newValue;
}
以上伪代码描述了一个由比较和赋值两阶段组成的复合操作,CAS 可以看作是它们合并后的整体——一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的,后面我会具体介绍。
Java 中的 CAS
在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作,如图。
它们都是 native 方法,由 Java 虚拟机提供具体实现,这意味着不同的 Java 虚拟机对它们的实现可能会略有不同。
下面我将通过代码演示一下它们的功能,以 compareAndSwapInt
为例。
首先需要得到 Unsafe 对象。由于 Unsafe 被设计为单例类,并且它的获取实例的方法只允许被基础类库中的类调用,因此,我们自己的类要想获取 Unsafe 对象,只能通过反射实现。
获取 Unsafe 对象的代码如下:
private static Unsafe getUnsafe() {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(Unsafe.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
Unsafe 的 compareAndSwapInt
方法接收 4 个参数,分别是:对象实例、字段偏移量、字段期望值、字段新值。该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作。
获取字段偏移量的代码如下:
private static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
演示代码如下:
public static void main(String[] args) {
Unsafe unsafe = getUnsafe();
long offset = getFieldOffset(unsafe, Entity.class, "x");
boolean successful;
successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
System.out.println(successful + "\t" + entity.x);
}
在我们的演示代码中,我们首先得到 Unsafe 对象,然后得到 Entity
中的 x
字段的偏移量(Entity 是我们自定义的实体类)。接下来是针对 entity.x
的 3 次 CAS 操作,分别试图将它从 0 改成 3、从 3 改成 5、从 3 改成 8。
执行结果如下:
可以看到,由于 entity.x
的原始值为 0
,所以第一次 CAS 成功地将它更新为 3
,第二次 CAS 也成功地将它更新为 5
,但是在第三次 CAS 时,由于 entity.x
的当前值 5
与期望值 3
不相同,所以 CAS 失败, entity.x
并没有得到更新,它的值仍然是 5
。
以上就是 CAS 在 Java 中的直观体现,它是所有并发原子类型的基础。下面我们来看一下它的底层实现。
JVM 中的 CAS
关于上面演示的 compareAndSwapInt
方法,Hotspot 虚拟机对它的实现如下:
为了更加直观,我在这里打上了断点,并联合上面的 Java 代码一起调试。上图显示了当前线程停在了断点处的对 Atomic::cmpxchg
方法的调用上。
Atomic::cmpxchg
方法非常关键,它是 Hotspot 虚拟机对 CAS 操作的封装。我们将断点跟进方法内部,从 “Variables” 标签页中可以观察到,当前 Java 虚拟机正在处理上述 Java 程序的第一次 CAS 请求,准备将 entity.x
的值从 0 改成 3,如图。
Atomic::cmpxchg
方法的定义如上图所示,它首先通过 os::is_MP()
判断当前执行环境是否为多处理器环境,然后嵌入一段汇编代码,这段汇编代码会执行一条 cmpxchgl
指令,同时把 exchange_value
等变量作为操作数,当它执行完成之后,方法将直接返回 exchange_value
的值。
从中可以看出, cmpxchgl
汇编指令是整个 Atomic::cmpxchg
方法的核心。
顺便补充一下,汇编代码中的
LOCK_IF_MP
是一个宏,这个宏的作用是,在多处理器环境下,为cmpxchgl
指令添加lock
前缀,以达到内存屏障的效果。内存屏障能够在目标指令执行之前,保障多个处理器之间的缓存一致性,由于单处理器环境下并不需要内存屏障,故做此判断。
cmpxchgl
指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,在我们的例子中,它会首先比较 dest
指针指向的内存值是否和 compare_value
的值相等,如果相等,则双向交换 dest
与 exchange_value
,否则就单方面地将 dest
指向的内存值交给 ``exchange_value。这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
事实上,现代指令集架构基本上都会提供 CAS 指令,例如 x86 和 IA-64 架构中的 cmpxchgl
指令和 comxchgq
指令,sparc 架构中的 cas
指令和 casx
指令等等。
不管是 Hotspot 中的 Atomic::cmpxchg
方法,还是 Java 中的 compareAndSwapInt
方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。
加载全部内容