背景
最近遇到了一段代码,仔细分析了下异常的原因,过程中遇到了很多问题,在这里总结下
public class NoVisiabilityTest {
private static class ReadThread extends Thread {
private boolean ready;
private int number;
public void run() {
while (!ready) {
number++;
//System.out.println("OK");
}
System.out.println(ready);
}
public void readyOn() {
this.ready = true;
}
}
public static void main(String[] args) throws InterruptedException {
ReadThread readThread = new ReadThread();
readThread.start();
Thread.sleep(200);
readThread.readyOn();
System.out.println(readThread.ready);
}
}
首先这是一段有问题的死循环代码,由于自己对于文中的解释不是特别理解,因此自己实践了下
问题解决
解决死循环的问题可以通过valotile命名ready变量,或者
while (!ready) {
number++;
System.out.println("OK");
}
相关原理你可以参考上述出处,或者继续跟下去。每个人的观点都不一样,我也不确保自己的见解一定是正确的
问题跟踪
简化代码
public class Thread1{
private boolean ready;
private int number;
public void run0() {
while (!ready) {
number++;
//System.out.println("OK");
}
System.out.println(ready);
}
public void readyOn() {
this.ready = true;
}
public static void main(String[] args) {
Thread1 inst = new Thread1();
//System.out.println("get ready");
inst.run0();
}
}
编译期
javap -c Thread1
Compiled from "Thread1.java"
public class Thread1 {
public Thread1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run0();
Code:
0: aload_0
1: getfield #2 // Field ready:Z
4: ifne 20
7: aload_0
8: dup
9: getfield #3 // Field number:I
12: iconst_1
13: iadd
14: putfield #3 // Field number:I
17: goto 0
20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_0
24: getfield #2 // Field ready:Z
27: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V
30: return
public void readyOn();
Code:
0: aload_0
1: iconst_1
2: putfield #2 // Field ready:Z
5: return
public static void main(java.lang.String[]);
Code:
0: new #6 // class Thread1
3: dup
4: invokespecial #7 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #8 // Method run0:()V
12: return
}
运行时
运行时优化比较难以定位,需要安装hsdis插件,环境搭建可以参照:http://www.infoq.com/cn/articles/zzm-java-hsdis-jvm/
CMD:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Thread1.run0 -XX:CompileCommand=compileonly,*Thread1.run0 Thread1 > T1.log
ASM:
Decoding compiled method 0x0000000110ac10d0:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Constants]
# {method} 'run0' '()V' in 'Thread1'
# [sp+0x20] (sp of caller)
0x0000000110ac1200: mov 0x8(%rsi),%r10d
0x0000000110ac1204: shl $0x3,%r10
0x0000000110ac1208: cmp %r10,%rax
0x0000000110ac120b: jne 0x0000000110a96960 ; {runtime_call}
0x0000000110ac1211: data32 xchg %ax,%ax
0x0000000110ac1214: nopl 0x0(%rax,%rax,1)
0x0000000110ac121c: data32 data32 xchg %ax,%ax
[Verified Entry Point]
0x0000000110ac1220: mov %eax,-0x14000(%rsp)
0x0000000110ac1227: push %rbp
0x0000000110ac1228: sub $0x10,%rsp ;*synchronization entry
; - Thread2::run0@-1 (line 8)
0x0000000110ac122c: mov %rsi,%r10
0x0000000110ac122f: movzbl 0x10(%rsi),%r11d
0x0000000110ac1234: test %r11d,%r11d
0x0000000110ac1237: jne 0x0000000110ac124c ;*ifne
; - Thread2::run0@4 (line 8)
0x0000000110ac1239: mov 0xc(%rsi),%r8d ;*getfield number
; - Thread2::run0@9 (line 9)
0x0000000110ac123d: inc %r8d ;*iadd
; - Thread2::run0@13 (line 9)
0x0000000110ac1240: mov %r8d,0xc(%r10) ; OopMap{r10=Oop off=68}
;*goto
; - Thread2::run0@17 (line 9)
0x0000000110ac1244: test %eax,-0x133124a(%rip) # 0x000000010f790000
;*goto
; - Thread2::run0@17 (line 9)
; {poll}
0x0000000110ac124a: jmp 0x0000000110ac123d
0x0000000110ac124c: mov $0x1c,%esi
0x0000000110ac1251: mov %r10,%rbp
0x0000000110ac1254: data32 xchg %ax,%ax
0x0000000110ac1257: callq 0x0000000110a97f20 ; OopMap{rbp=Oop off=92}
;*getstatic out
; - Thread2::run0@20 (line 11)
; {runtime_call}
0x0000000110ac125c: callq 0x000000011041bb72 ;*getfield number
; - Thread2::run0@9 (line 9)
; {runtime_call}
0x0000000110ac1261: callq 0x000000011041bb72 ; {runtime_call}
从0x0000000110ac123d到0x0000000110ac124a已经可以看出陷入了死循环(0x0000000110ac1244行轮询safepoint),而翻译过来的java代码类似于:
while (!ready) {
if(ready) {
System.out.println("OK");
} else {
while(true) {
number++;
}
}
}
一个uncommon_trap()引发的思考
代码如下:
public class Thread2{
private boolean ready;
private int number;
public void run0() {
while (!ready) {
number++;
System.out.println("OK");
}
System.out.println(ready);
}
public void readyOn() {
this.ready = true;
}
public static void main(String[] args) {
Thread1 inst = new Thread1();
//System.out.println("get ready");
inst.run0();
}
}
JIT反编译后的汇编如下:
[Verified Entry Point]
0x00000001104c1220: mov %eax,-0x14000(%rsp)
0x00000001104c1227: push %rbp
0x00000001104c1228: sub $0x10,%rsp ;*synchronization entry
; - Thread1::run0@-1 (line 8)
0x00000001104c122c: movzbl 0x10(%rsi),%r10d
0x00000001104c1231: test %r10d,%r10d
0x00000001104c1234: je 0x00000001104c1249 ;*ifne
; - Thread1::run0@4 (line 8)
0x00000001104c1236: mov %rsi,%rbp
0x00000001104c1239: mov $0x1e,%esi
0x00000001104c123e: nop
0x00000001104c123f: callq 0x0000000110497f20 ; OopMap{rbp=Oop off=68}
;*getstatic out
; - Thread1::run0@28 (line 12)
; {runtime_call}
0x00000001104c1244: callq 0x000000010fe1bb72 ;*getstatic out
; - Thread1::run0@28 (line 12)
; {runtime_call}
0x00000001104c1249: incl 0xc(%rsi) ;*synchronization entry
; - Thread1::run0@-1 (line 8)
0x00000001104c124c: mov %rsi,%rbp
0x00000001104c124f: mov $0x1e,%esi
0x00000001104c1254: data32 xchg %ax,%ax
0x00000001104c1257: callq 0x0000000110497f20 ; OopMap{rbp=Oop off=92}
;*getstatic out
; - Thread1::run0@17 (line 10)
; {runtime_call}
0x00000001104c125c: callq 0x000000010fe1bb72 ;*getstatic out
; - Thread1::run0@17 (line 10)
; {runtime_call}
......
OK
OK
OK
......
Decoding compiled method 0x00000001104bf3d0:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'run0' '()V' in 'Thread1'
0x00000001104bf540: callq 0x000000010fe1bb72 ; {runtime_call}
0x00000001104bf545: data32 data32 nopw 0x0(%rax,%rax,1)
0x00000001104bf550: mov %eax,-0x14000(%rsp)
0x00000001104bf557: push %rbp
0x00000001104bf558: sub $0x10,%rsp
0x00000001104bf55c: mov (%rsi),%rbp
0x00000001104bf55f: mov %rsi,%rdi
0x00000001104bf562: movabs $0x10fe75a70,%r10
0x00000001104bf56c: callq *%r10
0x00000001104bf56f: mov 0x8(%rbp),%r11d ; implicit exception: dispatches to 0x00000001104bf601
0x00000001104bf573: cmp $0xef610642,%r11d ; {oop('Thread1')}
0x00000001104bf57a: jne 0x00000001104bf5dc
0x00000001104bf57c: jmp 0x00000001104bf594
0x00000001104bf57e: xchg %ax,%ax ;*aload_0
; - Thread1::run0@0 (line 8)
0x00000001104bf580: lea (%r12,%r10,8),%rsi ;*getstatic out
; - Thread1::run0@17 (line 10)
0x00000001104bf584: movabs $0x7d5654668,%rdx ; {oop("OK")}
0x00000001104bf58e: nop
..................
上面的Hotspot log可以看到有两个问题:
1、该方法被执行12000+次会被二次编译了
2、第一次编译后的代码竟然没有jmp,看不出循环怎么实现的
咨询了下撒加,得到原因如下:
我用了-Xcomp,在第一次调用Thread2.run0()之前PrintStream类还没有加载。所以第一次编译得到的就是在while循环内和循环后各有一个uncommon_trap()的调用。于是就没有循环了,无论循环与否都跳回解释器去跑。跑到足够热了就触发了第二次编译。
可以参考博客:http://rednaxelafx.iteye.com/blog/1038324
Q&A
1、JVM不是解释执行的么,怎么会扯到汇编代码的?
方法被执行的次数+方法体内总循环的执行次数 > 阈值,会触发JIT编译成本地代码,你试着把Thread.sleep(200)去掉也就没有死循环了,但是随着时间的推移,势必又会出现死循环的情况
2、valotile命名变量可以解决这一问题,而一般并发程序中都是如此设计的,原理是什么?真的能彻底解决么?
其实从汇编代码可以推敲出valotile的原理,你可以写段代码试着JIT编译后参考下。
我更加赞同的观点是:volatile关键字防止了编绎器优化,所以对于变量不会被放到寄存器里,或者被优化掉。但是volatile并不能防止CPU从Cache中读取数据,可以参考博客:http://blog.youkuaiyun.com/hengyunabc/article/details/26822801