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 的价值所在。
JVM 源码分析(三):深入理解 CAS的更多相关文章
- JVM源码分析之synchronized实现
“365篇原创计划”第十二篇. 今天呢!灯塔君跟大家讲: JVM源码分析之synchronized实现 java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- JVM源码分析之Metaspace解密
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
- JVM源码分析-JVM源码编译与调试
要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析-类加载场景实例分析
A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...
- JVM源码分析之堆内存的初始化
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十五篇. 今天呢!灯塔君跟大家讲: JVM源码分析之堆内存的初始化 堆初始化 Java堆的初始化入口位于Univ ...
- JVM源码分析之JVM启动流程
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...
- JVM源码分析之Java对象头实现
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...
随机推荐
- 箭头函数this指向问题
this指向本质 箭头函数this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this.正是因为它没有this ...
- STL——容器(deque) 构造 & 头尾添加删除元素
1.deque容器概念 deque是"double-ended queue"的缩写,和vector一样都是STL的容器,唯一不同的是:deque是双端数组,而vector是单端的. ...
- Java8的Lambda表达式,你会不?
目录 理解Lambda 基础语法 函数式接口 常用的函数式接口 消费型接口 供给型接口 断言型接口 函数型接口 方法引用 数组引用 构造器引用 总结 参考阅读 理解Lambda Lambda表达式可以 ...
- Day2 之 元组tuple
tuple 元组 也是有序列表 ,与list非常相似,但是tuple一旦初始化就不能修改. name = ('a','b',1,2,3,True) tuple ...
- docker redis 设置和使用
1 开启docker 拉取redis镜像 1.1 桌面版docker 在镜像所在位置命令行执行 docker load -i redis.tar 1.2 开启redis docker run -p ...
- 二、TestNG的Hello World
创建第一个TestNG的例子 1.创建一个TestNG的类 选择项目路径"右键"--NEW--Other 选择TestNG cLass(如果没有这个选项是testng没有配置成功) ...
- vue+axois 封装请求+拦截器(请求锁+统一错误)
需求 封装常用请求 拦截器-请求锁 统一处理错误码 一.封装常用的请求 解决痛点:不要每一个模块的api都还要写get,post,patch请求方法.直接将这些常用的方法封装好. 解决方案:写一个类 ...
- Security篇:RememberMe
RememberMe功能 rememberMeServices接口 有AbstractRememberMeServices抽象类 PersistentTokenBasedRememberMeServi ...
- 学Python编程能做什么工作?从事什么岗位?——这些问题你知道吗?
前言 学Python编程能做什么工作?随着人工智能发展,学习python语言的人员有更多的岗位机会,python从事的职业广泛,从游戏到AI人工智能能都可以用Python实现.除了编程,各种岗位的人都 ...
- 职场PUA,管理者的五宗罪
在目前的社会环境下,程序员似乎成了"弱势群体".我们经常谈论的职场PUA已经成为程序员的代名词. 我一直在想,为什么这么多管理者能力会这么差. 但最后最吃亏的还是可怜的程序员. 也 ...