前言

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

鉴于 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. [JDK8]Map接口与Dictionary抽象类

    package java.util; 一.Map接口 接口定义 public interface Map<K,V> Map是存放键值对的数据结构.map中没有重复的key,每个key最多只 ...

  2. DVWA各级文件包含漏洞

    File Inclusion文件包含漏洞 漏洞分析 程序开发人员通常会把可重复使用的函数写入到单个文件中,在使用某些函数时,直接调用此文件,而无需再次编写,这种调用文件的过程被称为包含. 有时候由于网 ...

  3. java web简单的增删改查

    1.主要的文件,运行结果,运行界面,数据库创建的表等图片. 所要创建的文件和要导入的包: 主页面: 显示界面: 数据库的信息: 删除.查找.修改就不一 一列出来,自己可以运行看看.哈哈 2.接下来我将 ...

  4. html+css一些简单案例:爱心点击,盒子模型,2d动画

    canvas绘制爱心 效果预览 上代码 <!doctype html> <html> <head> <title>HTML5 Canvas爱心飘动动画特 ...

  5. mysql难题收录

    1.计算相邻两行的年龄的差距 表中的数据如下 select (s.age-(select age from stu where id - s.id = 1)) from stu as s; selec ...

  6. (byte & 0xff)操作

    先看一段代码: @Test public void test(){ byte a = -5; byte b = 12; System.out.println(a); System.out.printl ...

  7. 关于AES-CBC模式字节翻转攻击(python3)

    # coding:utf-8 from Crypto.Cipher import AES import base64 def encrypt(iv, plaintext): if len(plaint ...

  8. 持久层之 MyBatis: 第一篇:快速入门

    MyBatis入门到精通 JDBC回顾 1.1.认识MyBatis 1.1.使用IDEA创建maven工程 1.2.引入mysql依赖包 1.3.准备数据 1.4 使用JDBC手写MyBatis框架 ...

  9. Python手把手教程之用户输入input函数

    函数input() 函数 input() 让程序暂停运行,等待用户输入一些文本.获取用户输入后,Python将其存储在一个变量中,以方便你使用. 例如,下面的程序让用户输入一些文本,再将这些文本呈现给 ...

  10. 太酷炫了!!我会Python 导包八种方法,你呢?

    1. 直接 import 人尽皆知的方法,直接导入即可 >>> import os >>> os.getcwd() '/home/wangbm'复制代码 与此类似的 ...