JVM系列之:从汇编角度分析Volatile

简介

Volatile关键字对熟悉java多线程的朋友来讲,应该很熟悉了。Volatile是JMM(Java Memory Model)的一个很是重要的关键词。经过是用Volatile能够实现禁止重排序和变量值线程之间可见两个主要特性。java

今天咱们从汇编的角度来分析一下Volatile关键字究竟是怎么工做的。多线程

重排序

这个世界上有两种重排序的方式。架构

第一种,是在编译器级别的,你写一个java源代码,通过javac编译以后,生成的字节码顺序可能跟源代码的顺序不一致。jvm

第二种,是硬件或者CPU级别的重排序,为了充分利用多核CPU的性能,或者CPU自身的处理架构(好比cache line),可能会对代码进行重排序。好比同时加载两个非互相依赖的字段进行处理,从而提高处理速度。oop

咱们举个例子:性能

public class TestVolatile {

    private static int int1;
    private static int int2;
    private static int int3;
    private static int int4;
    private static int int5;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            increase(i);
        }
        Thread.sleep(1000);
    }

    private static void increase(int i){
        int1= i+1;
        int2= i+2;
        int3= i+3;
        int4= i+4;
        int5= i+5;
    }
}

上面例子中,咱们定义了5个int字段,而后在循环中对这些字段进行累加。spa

先看下javac编译出来的字节码的顺序:线程

咱们能够看到在设置值的过程当中是和java源代码的顺序是一致的,是按照int1,int2,int3,int4,int5的顺序一个一个设置的。指针

而后咱们看一下生成的汇编语言代码:code

在运行是添加参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline,或者直接使用JIT Watcher。

从生成的代码中,咱们能够看到putstatic是按照int1,int5,int4,int3,int2的顺序进行的,也就是说进行了重排序。

若是咱们将int2设置成为Volatile,看看结果如何?

前方高能预警,请小伙伴们作好准备

咱们先看putstatic的顺序,从注释里面,咱们只发现了putstatic int2, int3和int5。

且慢!咱们不是须要设置int1,int2,int3,int4,int5 5个值吗?这里怎么只有3个。

要是没有能独立思考和独立决定的有创造我的,社会的向上发展就不可想像 - 爱因斯坦

这里是反编译的时候注释写错了!

让咱们来仔细分析一下汇编代码。

第一个红框,不用懂汇编语言的朋友应该也能够看懂,就是分别给r11d,r8d,r9d,ecx和esi这5个寄存器分别加1,2,3,4,5。

这也分别对应了咱们在increase方法中要作的事情。

有了这些寄存器的值,咱们再继续往下看,从而能够知道,第二个红框实际上表示的就是putstatic int1,而最后一个红框,表示的就是putstatic int4。

因此,你们必定要学会本身分析代码。

5个putstatic都在,同时由于使用了volatile关键字,因此int2做为一个分界点,不会被重排序。因此int1必定在int2以前,而int3,4,5必定在int2以后。

上图的结果是在JIT Watcher中的C2编译器的结果,若是咱们切换到C1编译器:

此次结果没错,5个int都在,同时咱们看到这5个int竟然没有重排序。

这也说明了不一样的编译器可能对重排序的理解程度是不同的。

写的内存屏障

再来分析一下上面的putstatic int2:

lock addl $0x0,-0x40(%rsp)  ;*putstatic int2 {reexecute=0 rethrow=0 return_oop=0}

这里使用了 lock addl指令,给rsp加了0。 rsp是SP (Stack Pointer) register,也就是栈指针寄存器。

给rsp加0,是否是很奇怪?

加0,虽然没有改变rsp的值,可是由于前面加了lock,因此这个指令会被解析为内存屏障。

这个内存屏障保证了两个事情,第一,不会重排序。第二,全部的变量值都会回写到主内存中,从而在这个指令以后,变量值对其余线程可见。

固然,由于使用lock,可能对性能会有影响。

非lock和LazySet

上面咱们提到了volatile会致使生成lock指令。

但有时候,咱们只是想阻止重排序,对于变量的可见性并无那么严格的要求。

这个时候,咱们就可使用Atomic类中的LazySet:

public class TestVolatile2 {

    private static int int1;
    private static AtomicInteger int2=new AtomicInteger(0);
    private static int int3;
    private static int int4;
    private static int int5;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            increase(i);
        }
        Thread.sleep(1000);
    }

    private static void increase(int i){
        int1= i+1;
        int2.lazySet(i+2);
        int3= i+3;
        int4= i+4;
        int5= i+5;
    }
}

从结果能够看到,int2没有重排序,也没有添加lock。s

注意,上面的最后一个红框表示的是putstatic int4。

读的性能

最后,咱们看下使用volatile关键字对读的性能影响:

public class TestVolatile3 {

    private static volatile int int1=10;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            readInt(i);
        }
        Thread.sleep(1000);
    }

    private static void readInt(int i){
        if(int1 < 5){
            System.out.println(i);
        }
    }
}

上面的例子中,咱们对int1读取10000次。看下编译结果:

从结果能够看出,getstatic int1和不使用volatile关键字,生成的代码是同样的。

因此volatile对读的性能不会产生影响。

总结

本文从汇编语言的角度再次深刻探讨了volatile关键字和JMM模型的影响,但愿你们可以喜欢。

本文做者:flydean程序那些事

本文连接:http://www.flydean.com/jvm-volatile-assembly/

本文来源:flydean的博客

欢迎关注个人公众号:程序那些事,更多精彩等着您!

相关文章
相关标签/搜索