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

一、实验环境:

1、Idea代码编辑器

2、jdk1.8.0_92

3、win10_x64

 

二、易产生误解的Java字段Volatile

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

1.volatile关键字的两层语义

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

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  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)的原子指令,使得修改是原子操作。如果修改失败,则继续尝试,知道成功。

 

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

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

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

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

 

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

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

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

 

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

18212541_r4kI.jpg

 

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

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

 

 

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

18212541_8ufe.jpg

 

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

volatile字节码为:

18212542_HF53.jpg

 

内存屏障有两个能力:

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对原子指令保证的官方文档:

18212542_NUgW.jpg

 

18212542_r6kf.jpg

 

18212543_Sffk.jpg

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

 

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

在分析synchronizied时候,由于汇编代码比较多,因此先将java代码编译成字节码:

18212543_Dtya.jpg

查看test方法字节码:

18212544_aJtC.jpg

 

18212544_d6cp.jpg

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

 

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

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

 

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

1、创建工程:

18212544_PPUO.jpg

 

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

18212544_U1mj.jpg

 

18212545_ZaJT.jpg

 

18212545_eHVJ.jpg

 

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

 

3、再次运行:

18212545_8FwZ.jpg

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

 

4、下载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对应目录下面:

18212545_Cklr.jpg

 

5、继续运行程序:

18212546_H2h0.jpg

 

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

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:禁止内联优化

 

 

6、对比使用普通变量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++;
        }
    }
}

18212546_z6Mo.jpg

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

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

 

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

normal.txt

 

18212547_BA4R.jpg

 

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

volatile.txt

 

通过beyondCompare比较:

18212547_IBPF.jpg

 

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

18212548_cYlw.jpg

 

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

 

 

7、再来实验,test方法增加synchronized关键字:

18212548_BhdE.jpg

 

synchronized-method.txt

 

 

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

18212548_CNBP.jpg

 

synchronized-i++.txt

 

18212548_VXZ5.jpg

 

 

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

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

 

 

参考引用:

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

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

转载于:https://my.oschina.net/tantexian/blog/808032

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值