5

JVM 源码分析(三):深入理解 CAS

 3 years ago
source link: https://www.daqianduan.com/20063.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
  • 前言
  • 什么是 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 操作,如图。

eama2qa.png!mobile

它们都是 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。

执行结果如下:

iIJZV3U.png!mobile

可以看到,由于 entity.x 的原始值为 0 ,所以第一次 CAS 成功地将它更新为 3 ,第二次 CAS 也成功地将它更新为 5 ,但是在第三次 CAS 时,由于 entity.x 的当前值 5 与期望值 3 不相同,所以 CAS 失败, entity.x 并没有得到更新,它的值仍然是 5

以上就是 CAS 在 Java 中的直观体现,它是所有并发原子类型的基础。下面我们来看一下它的底层实现。

JVM 中的 CAS

关于上面演示的 compareAndSwapInt 方法,Hotspot 虚拟机对它的实现如下:

JjeEzm3.png!mobile

为了更加直观,我在这里打上了断点,并联合上面的 Java 代码一起调试。上图显示了当前线程停在了断点处的对 Atomic::cmpxchg 方法的调用上。

Atomic::cmpxchg 方法非常关键,它是 Hotspot 虚拟机对 CAS 操作的封装。我们将断点跟进方法内部,从 “Variables” 标签页中可以观察到,当前 Java 虚拟机正在处理上述 Java 程序的第一次 CAS 请求,准备将 entity.x 的值从 0 改成 3,如图。

jyyuQr.png!mobile

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 的值相等,如果相等,则双向交换 destexchange_value ,否则就单方面地将 dest 指向的内存值交给 “exchange_value 。这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。

事实上,现代指令集架构基本上都会提供 CAS 指令,例如 x86 和 IA-64 架构中的 cmpxchgl 指令和 comxchgq 指令,sparc 架构中的 cas 指令和 casx 指令等等。

不管是 Hotspot 中的 Atomic::cmpxchg 方法,还是 Java 中的 compareAndSwapInt 方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK