简介

Volatile关键字对相熟java多线程的敌人来说,应该很相熟了。Volatile是JMM(Java Memory Model)的一个十分重要的关键词。通过是用Volatile能够实现禁止重排序和变量值线程之间可见两个次要个性。

明天咱们从汇编的角度来剖析一下Volatile关键字到底是怎么工作的。

重排序

这个世界上有两种重排序的形式。

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

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

咱们举个例子:

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字段,而后在循环中对这些字段进行累加。

先看下javac编译进去的字节码的程序:

咱们能够看到在设置值的过程中是和java源代码的程序是统一的,是依照int1,int2,int3,int4,int5的程序一个一个设置的。

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

在运行是增加参数-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的博客

欢送关注我的公众号:程序那些事,更多精彩等着您!