前言

在上一篇文章中,我们完成了源码的编译和调试环境的搭建。

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

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

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

JVM 源码分析(三):深入理解 CAS的更多相关文章

  1. JVM源码分析之synchronized实现

    “365篇原创计划”第十二篇.   今天呢!灯塔君跟大家讲:   JVM源码分析之synchronized实现     java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...

  2. JVM源码分析之一个Java进程究竟能创建多少线程

    JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...

  3. JVM源码分析之堆外内存完全解读

    JVM源码分析之堆外内存完全解读   寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...

  4. JVM源码分析之Metaspace解密

        概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...

  5. JVM源码分析-JVM源码编译与调试

    要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...

  6. JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)

    概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...

  7. JVM源码分析-类加载场景实例分析

    A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...

  8. JVM源码分析之堆内存的初始化

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十五篇. ​ 今天呢!灯塔君跟大家讲: JVM源码分析之堆内存的初始化   堆初始化 Java堆的初始化入口位于Univ ...

  9. JVM源码分析之JVM启动流程

      原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...

  10. JVM源码分析之Java对象头实现

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...

随机推荐

  1. Day1 【Scrum 冲刺博客】

    (因发作业当天没注意看作业内容,第一天的冲刺博客和第二天的同时发!!!不好意思!!!) 各个成员在 Alpha 阶段认领的任务 方晓莹 搭建社区管理系统的前端框架 登录页开发 管理员模块个人中心开发 ...

  2. 实战演习:mysqlbinlog恢复bin-log数据

    mysqlbinlog恢复bin-log数据 Binlog日志即binary log,是二进制日志文件,有两个作用,一个是增量备份,另一个是主从复制,即从节点同步主节点数据时获取的即是bin-log, ...

  3. Day7 python高级特性-- 切片 Slice

    先举一个例子,取list或tuple中的某几个元素:     1.取 ['a','b','c','d','e','f'] 第1.2.5.6个元素:        >>> a = [' ...

  4. js上 五、运算符-1

    5.1.认识运算符 什么是运算符? 运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算. 运算符的应用: 购物车:计算总价,数量: **Js ** 中有哪些运算符? 算术运算符.赋值运算符 ...

  5. Eureka部署在阿里云所带来的问题

    没有那么多废话,直奔主题... 1.解决查看eureka界面时服务名显示而非ip+端口,以及解决显示ip而非阿里云公网ip问题(个人解决方式,如果和我这样配置还是不行,那就再百度或者谷歌下吧) eur ...

  6. Web服务器-并发服务器-多进程(3.4.1)

    @ 目录 1.优化分析 2.代码 3. 关于作者 1.优化分析 在单进程的时候,相当于 是来一个客户,派一个人去服务一下 效率低,现在使用多进程来服务 假设场景 100个人同时访问页面 单进程:一次处 ...

  7. 使用轮询&长轮询实现网页聊天室

    前言 如果有一个需求,让你构建一个网络的聊天室,你会怎么解决? 首先,对于HTTP请求来说,Server端总是处于被动的一方,即只能由Browser发送请求,Server才能够被动回应. 也就是说,如 ...

  8. Python求一个数字列表的元素总和

    Python求一个数字列表的元素总和.练手: 第一种方法,直接sum(list): 1 lst = list(range(1,11)) #创建一个1-10的数字列表 2 total = 0 #初始化总 ...

  9. sql 查询条件为拼接字符串 不能使用IN 使用patindex查询结果集

    题目: 求组织机构ID在('5dc8de20-9f2f-465e-afcc-f69abecaee50','63549b63-1e0d-4269-98f4-013869d7f211','f7316bf3 ...

  10. Loki 初体验

    Loki 是什么 Loki 是 Grafana Lab开发的一套日志系统,使用Go语言实现.根据官方的介绍, Loki,高可用性,多租户的日志聚合系统,受到Prometheus的启发.它的设计非常经济 ...