精确解释java的volatile之可见性、原子性、有序性(经过汇编语言)

1、实验环境:html

一、Idea代码编辑器java

二、jdk1.8.0_92linux

三、win10_x64macos

 

2、易产生误解的Java字段Volatilewindows

volatile保证了可见性,可是并不保证原子性!!!缓存

1.volatile关键字的两层语义安全

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:多线程

  1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。jvm

  2)禁止进行指令重排序。编辑器

 

volatile的可见性,即任什么时候刻只要有任何线程修改了volatile变量的值,其余线程总能获取到该最新值。具体更多实现能够参阅缓存一致性协议。

 

2.那么volatile为何又不能保证原子性呢?

以volatile int i = 10;i++;为例分析:

i++实际为load、Increment、store三个操做。

某一时刻线程1将i的值load取出来,放置到cpu缓存中,而后再将此值放置到寄存器A中,而后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,所以其余线程并不会获取到这个自增1的值)。若是在此时线程2也执行一样的操做,获取值i==10,自增1变为11,而后立刻刷入主内存。此时因为线程2修改了i的值,实时的线程1中的i==10的值缓存失效,从新从主内存中读取,变为11。接下来线程1恢复。将自增事后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。

 

3.synchronized相对于volatile又是如何保证原子性呢?

volatile:从最终汇编语言从面来看,volatile使得每次将i进行了修改以后,增长了一个内存屏障lock addl $0x0,(%rsp)保证修改的值必须刷新到主内存才能进行内存屏障后续的指令操做。可是内存屏障以前的指令并非原子的。

synchronized:则是使用lock cmpxchg %rsi,(%rdi)的原子指令,使得修改是原子操做。若是修改失败,则继续尝试,知道成功。

 

3、Java字节码与汇编语言关系(解释性语言仍是编译语言?)

首先咱们简要解释下java语言应该是编译成字节码、为何会和汇编语言有联系?

现在,基于物理机、虚拟机等的语言,大多都遵循这种基于现代经典编译原理的思路,在执行前先对程序源码进行词法解析和语法解析处理,把源码转化为抽象语法树。对于一门具体语言的实现来讲,词法和语法分析乃至后面的优化器和目标代码生成器均可以选择独立于执行引擎,造成一个完整意义的编译器去实现,这类表明是C/C++语言。也能够把抽象语法树或指令流以前的步骤实现一个半独立的编译器,这类表明是Java语言。又或者能够把这些步骤和执行引擎所有集中在一块儿实现,如大多数的JavaScript执行器。

Java便是解释性语言,又是编译器语言。Java支持两种方式同时进行。因为解释性语言性能相对较慢,所以Java用了JIT技术,将频繁执行的代码编译成本地机器语言,这样后续既能够直接运行。所以使用JIT技术可以获取到机器汇编语言。

 

4、直接从汇编入手分析volatile及synchronized多线程问题

下述经过四种方式代码的汇编指令比较,下面针对关于线程安全相关的汇编指令进行重点分析。

PS:具体关于如何获取汇编代码,及4种方式java源代码,请参考第五章。

 

一、普通方式int i,执行i++:

 

普通方式没有任何与锁有关的指令;其余方式都出现了与锁相关的汇编指令lock。

解释指令:其中edi为32位寄存器。若是是long则为64位的rdi寄存器。

 

 

二、volatile方式volatile int i,执行i++:

 

指令“lock; addl $0,0(%%esp)”表示加锁,把0加到栈顶的内存单元,该指令操做自己无心义,但这些指令起到内存屏障的做用,让前面的指令执行完成。具备XMM2特征的CPU已有内存屏障指令,就直接使用该指令。

volatile字节码为:

 

内存屏障有两个能力:

1. 阻止屏障两边的指令重排序

2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

 

对Load Barrier来讲,在读指令前插入读屏障,可让高速缓存中的数据失效,从新从主内存加载数据

对Store Barrier来讲,在写指令以后插入写屏障,能让写入缓存的最新数据写回到主内存。

 

关于原子性解释:

上述volatile方式的i++,总共是四个步骤:

Load、Increment、Store、Memory Barriers。

Memory Barriers步骤保证了jvm让这个最新的变量的值在全部线程可见,也就是最后一步让全部的CPU内核都得到了最新的值,但中间的几步(从Load到Increment到Store)是不安全的,中间若是其余的CPU修改了值将会丢失。

为何从Load到Increment到Store三个指令不是原子性的,请参考intex对原子指令保证的官方文档:

 

 

文档地址:https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

 

三、synchronizied方式int i,使用synchronized锁住i++:

在分析synchronizied时候,因为汇编代码比较多,所以先将java代码编译成字节码:

查看test方法字节码:

 

上述汇编代码可知,monitorenter与monitorexit包裹了getstatic i及putstatic i,等相关代码执行指令。中间值得交换采用了原子操做lock cmpxchg %rsi,(%rdi),若是交换成功,则执行goto直接退出当前函数return。若是失败,执行jne跳转指令,继续循环执行,直到成功为止。

 

jne指令:是一个条件转移指令。当ZF=0,转至标号处执行。

cmpxchg指令:比较rsi和目的操做数rdi(第一个操做数)的值,若是相同,ZF标志被设置,同时源操做数(第二个操做)的值被写到目的操做数,不然,清ZF标志为0,而且把目的操做数的值写回rsi,则执行jne跳转指令。。

 

5、获取四种形式的Java代码对应的汇编指令

一、建立工程:

 

二、配置jvm参数,使之能输出汇编语言:

 

 

 

须要添加的JVM参数为:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test(其中Test为class类名)

 

三、再次运行:

报错说明:Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled

 

四、下载hsdis-amd64.dll插件:

https://kenai.com/projects/base-hsdis/downloads(网站上面只提供了linux、macos、Solaris等版本下载)

windows版本的hsdis-amd64.dll插件须要自行build:http://dropzone.nfshost.com/hsdis.htm

 

此处给你们提供已经编译好的dll文件:

http://pan.baidu.com/s/1bpIIzHd

 

下载完成放置到windows的jdk对应目录下面:

 

五、继续运行程序:

 

输出的参数太多,可使用过滤输出:

Filtering Output

The -XX:+PrintAssembly option prints everything. If that's too much, drop it and use one of the following options.

Individual methods may be printed:

  • CompileCommand=print,*MyClass.myMethod prints assembly for just one method
  • CompileCommand=option,*MyClass.myMethod,PrintOptoAssembly (debug build only) produces the old print command output
  • CompileCommand=option,*MyClass.myMethod,PrintNMethods produces method dumps

These options accumulate.

If you get no output, use -XX:+PrintCompilation to verify that your method is getting compiled at all.

 

若是只是但愿打印某一个方法的汇编将JVM参数设置为:

java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test

 

其中:

-Xcomp表示永远以编译模式运行(禁止解释器模式)

-XX:-Inline:禁止内联优化

 

 

六、对比使用普通变量i及volatile变量的汇编指令对比:

package main.java;

/*
 * 使用汇编语言来验证volatile
 *
 * @author tantexian<my.oschina.net/tantexian>
 * @since 2016/12/17
 *
 * @params java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test
 *
 */
public class Test {
    static int i = 888;
    //static volatile int i = 888;

    public static void main(String[] args){
        test();
    }

    public static void test() {
        synchronized (Test.class) {
            i++;
        }
    }
}

PS:后续实验的执行参数都为:

java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test

 

普通变量执行后结果输出:

normal.txt

 

 

volatile变量修饰后执行后结果输出:

volatile.txt

 

经过beyondCompare比较:

 

再次补充实验下long的指令:将volatile int i修给为 volatile long i:

 

注意:movabsq不是32位的扩展,是纯新增的指令。用来将一个64位的字面值直接存到一个64位寄存器中。由于movq只能将32位的值存入,因此新增了这样一条指令。rdi与r10为64位寄存器(r为64位寄存器前缀,e为32位寄存器前缀)。即将数字1放置到64位寄存器中。add %r10,%rdi将j+1结果保存在rdi中。getstatic用来获取类的一个静态字段值。

 

 

七、再来实验,test方法增长synchronized关键字:

 

synchronized-method.txt

 

 

八、再来实验,test的i++代码段使用synchronized关键字:

 

synchronized-i++.txt

 

 

 

上述四次实验汇编代码打包文件地址:http://pan.baidu.com/s/1nuZIOdj

Java四种条件下汇编指令.rar

 

 

参考引用:

https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly

http://www.cnblogs.com/Mainz/p/3556430.html#

相关文章
相关标签/搜索