程序地带

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


前言什么是 CASJava 中的 CASJVM 中的 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 + " " + entity.x);    successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);    System.out.println(successful + " " + entity.x);    successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);    System.out.println(successful + " " + 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 的价值所在。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/yonghengzh/p/14277544.html

随机推荐

郑轻1036——某年某月有多少天

题目描述给你一个年份和月份,求该月有多少天输入一个年份(正整数),一个月份(1-12),中间有一个空格...

蔡高兴, 阅读(274)

2020-12-08

2020-12-08

为啥我图片不能下载一打开就是空白...

特别有套路的他 阅读(119)

go get 本地_go-micro+php+consul简单的微服实现

go get 本地_go-micro+php+consul简单的微服实现

最近segmentfault一直给我推送一个叫Hyperf的swoole写的php框架,点进去一看发现官方支持微服务架构,然后顺手研究一下,发现还挺好用的。恰...

weixin_39614146 阅读(964)

Spring使用@ComponentScan注解不生效

今天使用Spring写个小案例,发现@ComponentScan注解不生效,我去了个DJ,什么原因,下面贴下目录结构与代码:...

世代农民 阅读(665)

go float64 比较_Go 每日一库之 plot

Go每日一库之plot简介本文介绍Go语言的一个非常强大、好用的绘图库——plot。plot内置了很多常用的组件,基本满足日常需求。同时,它也提供了定制化的接口࿰...

weixin_39631263 阅读(460)