volatile变量基于内存屏障实现可见性
Java程序在运行时, 最终都是需要被编译成机器指令交给CPU执行的, 为了提高程序的执行性能, 编辑器和处理器都有可能会对指令进行重排序。
从Java源代码到最终执行的指令序列, 会分别经历下面三个重排序:编译器重排序, CPU指令重排序,内存系统重排序。其中CPU指令重排序和内存系统重排序属于处理器重排序。在不改变单线程程序语义的前提下,只要最终执行结果是不变的,编译器可以重新安排语句的执行顺序。
多线程并发时,在未采取任何同步措施的情况下,重排序有可能会造成不可预知的数据一致性问题,那有没有什么办法来禁止重排序呢?
在计算机指令中,可以使用内存屏障指令,也称为内存栅栏,禁止处理器的重排序。内存屏障是一组CPU指令, 它的作用就是处理器重排序, 强制数据访问内存的顺序和程序代码顺序一样, 禁止指令越过内存屏障执行。内存屏障是一种标准,不同的处理器厂商和操作系统可能会采用不同的实现。
在Java内存模型中使用内存屏障可以禁止编译器和处理器的重排序, 在后面有详细说明。
这样, 在Java并发编程时, 只需要确保代码执行顺序(禁止重排序) 时, Java编译器会在适当位置插入一个内存屏障命令来禁止特定类型的重排序, 相当于告诉处理器和JVM编译器先于这个命令的必须先执行,后于这个命令的必须后执行。volatile变量就是是基于内存屏障实现的。下面我们从源码来解析。
步骤0:下载源码
Open JDK的源码是托管在Mercurial代码版本管理平台上的, 可以使用Mercurial的代码管理工具直接从远程仓库(Repository) 中下载获取源码。
我们选用的项目是Open JDK8u, 代码远程仓库地址:https://hg.openjdk.java.net/jdk8u/jdk8u/。
因为下载源码过程中需要执行脚本文件get_source.sh, 所以需要在Linux平台下下载。
大家可以在安装了Linux环境的机器上下载, 或者在自己的机器上安装Linux虚拟机后进行下载。
获取源代码过程如下:
Open JDK目录结构
步骤1:hsd is工具查看机器指令
使用hsd is可以查看Java编译后的机器指令。
使用方法:把编译好的hsd is-amd 64.dll放在$AVA_HOME/jre/bin/server目录下。
就可以使用如下命令,查看程序的机器指令。
java-XX:+Unlock Diagnostic VM Options-XX:+Print Assembly, 类名
在类Volatile Field Test中属性volatile Field为volatile变量。
在Linux中, 执行
java-XX:+Unlock Diagnostic VM Options-XX:+Print Assembly, Volatile Field Test
对于volatile修饰的变量进行写操作, 在生成汇编代码中, 会有如下的指令:
//内存屏障
lock addl$0x 0, (%esp) ;
这个操作相当于一个内存屏障。
●lock指令:会使紧跟在其后的指令变成原子操作, lock指令是一个汇编层面的指令, 作为前缀加载其他汇编指令之前,可以保证其后汇编操作的原子性。
这个操作相当于一个内存屏障, 在多CPU环境中, 置上LOCK指令可以确保一个处理器能独占使用共享内存。
●addl指令:加法操作指令。addl 0, 0(0, 0(%%esp) 使用此汇编指令再配合lock指令, 实现了cpu的内存屏障。
步骤2:查看源码
下面我们通过Open JDK源码, 看看JVM是怎么实现volatile的。
● 查看volatile字段字节码:ACC_VOLATILE
通过java p命令(java p-v-p Volatile Field Class.class) 可以看到volatile的属性的字节码flags标识中有个关键字ACC_VOLATILE。
不同的计算机架构(操作系统和CPU架构) 下由于内存屏障实现细节的不同, JVM的volatile底层实现也是不同的, 下面我们以linux_x 86为例, 来一层层解开volatile的面纱。
●is_volatile方法:判断一个变量是否是volatile类型
通过这个关键字ACC_VOLATILE我们可以定位到JVM源文件vm甥楴瑬iesaccess Flags.hpp文件, 代码如下:
public:
可以找到
bool is_volatile() const{return(_flags&JVM_ACC_VOLATILE) !=0; }
is_volatile这个函数是判断变量字节码中是否有ACC_VOLATILE这个flag。
●Java中volatile变量赋值:C++实现
字节码会通过Bytecode Interpreter解释器来执行, 在bytecode l nter preter.cpp文件再根据关键字
is_volatile搜索, 看到如下代码:
在这段代码中, cache->is_volatile() 这段代码, cache代表变量在常量池缓存中的实例(本例中为volatile Field) , 这段代码的作用是判断变量是否被volatile修饰。接着, 根据当前变量的类型来赋值,会先判断to s_type变量(volatile变量类型) , 后面有不同的基础类型的调用, 比如int类型就调用release_int_field_put。
release_int_field_put这个方法的实现在文件oop.inline.hpp中
inline void oop Desc::release_int_field_put(int offset, jint contents)
Order Access::release_store(int_field_addr(offset) , contents) ;
}
赋值动作int_field_addr外面包装执行了Order Access::release_store。
我们看看OrderAccess:release_store做了什么, 它的定义在vmuntimeorder Access.hpp中
static void release_store() ;
linux_x 86中实现在os_cpulinux_x 86vmorder Access_linux_x 86.inline.hpp
可以看到volatile操作, 第一步是实现了C++的volatile变量的原生赋值实现。
C++中的volatile关键字, 用来修饰变量, 是语言级别的内存屏障, 被volatile声明的变量, 编译器不会对(读/写)该变量的代码进行优化(重排序),且变量的值都必须直接写入内存地址或从内存地址读取。
●volatile使用内存屏障禁止重排序
赋值操作完成以后,我们可以看到最后一行执行的语句是
Order Access::store load() ;
它是JVM的store load一个内存屏障。
JMM把内存屏障指令分为了四类, 可以在vmuntimeorder Access.hpp找到对应的实现方法:
内存屏障的解释也可以在order Access.hpp该文件中看到, 如下:
其中Store Load Barriers是一个"全能型"的屏障, 它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持),执行该屏障开销会很昂贵。
由于在不同的处理器上(不同的处理器有不同的指令集) , JVM对同一类型的内存屏障会有不同的实现方式。以linux_x 86为例, 我们可以在os_cpulinux_x 86vmorder Access_linux_x 86.inline.hpp看到它们的实现:
inline void Order Access::load load() {acquire() ; }
inline void Order Access::store store() {release() ;}
inline void Order Access::load store() {acquire() ; }
inline void Order Access::store load() {fence() ; }
当使用store load屏障时, 会调用fence() 方法
os::is_MP(),会判断当前环境是否是多核,单核不存在内存不可见和代码乱序的问题,所以在多核CPU才需要使用内存屏障。
cc代表的是寄存器, memory代表是内存。同时使用"cc"和"memory"会通知编译器和处理器,volatile变量在内存或者寄存器内的值已经发生了修改,要重新加载,需要直接从主内存中读取。
至此, 我们就揭开了volatile的面纱, 知道了volatile保证有序性和可见性的原理, 在日后的工作中大家能更加熟练的运用volatile关键字了。
以上就是酷仔今日整理的“Java教程:volatile变量实现原理和具体实现源码”一文,本文承接Java基础教程的前两篇文章,均是对Java-volatile变量的解释说明,有相关学习需求的同学可以将前两篇文章一起连起来进行学习。