共计 2919 个字符,预计需要花费 8 分钟才能阅读完成。
简介
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 的博客
欢送关注我的公众号: 程序那些事,更多精彩等着您!