0.接受实践检验
实践是检验真理的唯一标准这句话在哲学上未必站的住脚,但经由实践或者实际问题检验自己对理论的理解,却是非常必要和重要的。近期看了不少JMM及volatile相关的东西,觉得大体明白了。无意间看到leetcode上网友发的一道字节面试题,方知远矣。
1.字节面试原题
为什么int a = v.s 会影响线程1的正常停止?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Vol {
boolean run = true;
volatile int s = 1;
public static void main(String[] args) throws InterruptedException {
Vol v = new Vol();
//thread 1
new Thread(() ->{
while (v.run) {
int a = v.s; //如果注释这行,线程1无法中止
}
}).start();
//thread 2
new Thread(() ->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
v.run = false;
}).start();
}
}
原题链接1
1.1 关于原问题链接下的答案
其中来自知乎的一个回答2挺好,并且利用jitwatch从字节码和二进制码角度实证了注释前后jvm内部的不同实现。但我认为话术表达略有问题(作者肯定是明白的),或者说逻辑略有问题。jvm编译结果也需要遵循一定的JMM原则,这个原则到底是什么,没有说清楚。经过一些搜索研究,我倾向于认为piggyback这个基于volatile的增强特性才是问题的根本。
JMM这个模型,还是有一定难度和复杂度。专家给出原则,但在具体的例子上并不都容易适用;给出例子,又无法穷尽所有情况。所以我暂时给出个人倾向的答案,以后有修订的可能。
2.piggyback
piggyback on
piggyback on somebody/something to use something that already exists as a support for your own work; to use a larger organization, etc. for your own advantage
大体就是利用别人或者别的事物来达成自己的目的,搭便车。
2.1 解释13
任何写ready变量之前的操作,对读取ready变量之后的操作都可见。因此,number变量利用了由ready变量强制执行的内存可见性。简单来说,尽管它不是一个volatile变量,但它表现出了volatile变量的行为
1
2
3
4
5
6
public class TaskRunner {
private static int number; // not volatile
private volatile static boolean ready;
// same as before
}
2.2 解释2: Full volatile Visibility Guarantee4
Java中的volatile关键字提供的内存可见性保证超出了volatile变量本身。其内存可见性保证如下:
- 如果线程A写入一个
volatile变量,然后线程B随后读取同一个volatile变量,那么线程A在写入volatile变量之前可见的所有变量,在线程B读取完volatile变量后也将对线程B可见。 - 如果线程A读取一个
volatile变量,那么线程A在读取volatile变量时可见的所有变量也将从主内存中重新读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
totalDays() 方法在读取 days 的值时,months 和 years 的值也会从主内存读取。因此通过上述读取顺序,可以确保看到 days、months 和 years 的最新值。
2.3 JSR 133 JMM模型的增强语义
上述增强理解可以参考5Brian Goetz写的一篇解释文章。
3.volatile变量与while的相对位置
3.1 之前
将int a = v.s置与while之前
1
2
3
4
5
6
//thread 1
new Thread(() ->{
int a = v.s;
while (v.run) {
}
}).start();
3.2 之后
将int a = v.s置与while之前
1
2
3
4
5
6
//thread 1
new Thread(() ->{
while (v.run) {
}
int a = v.s;
}).start();
3.3 结果
thread1在上述两种情况都是不能正常退出的。这点跟while在jvm中如何编译有关,是作为一个代码块整体处理的(待以后深究)。
4.编译结果对比
默认环境jdk1.8,后续文章同。
4.1 volatile变量注释后结果
1
2
3
4
5
6
//thread 1
new Thread(() ->{
while (v.run) {
// int a = v.s; //注释后thread1无法正常退出
}
}).start();
| 注释后结果 |
|---|
![]() |
4.2 未注释结果
1
2
3
4
5
6
//thread 1
new Thread(() ->{
while (v.run) {
int a = v.s; //注释后thread1无法正常退出
}
}).start();
| 未注释结果 |
|---|
![]() |
5.jitwatch
jitwatch是一个图形化分析查看jit编译字节码和二进制代码的工具6&7,支持导入日志分析,或者sandbox直接编译java代码。
若要在业务代码输出日志,可添加如下jvm参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jdk8:
-XX:+UnlockDiagnosticVMOptions
-XX:+TraceClassLoading
-XX:+LogCompilation
-XX:+PrintAssembly
-XX:+DebugNonSafepoints
-XX:LogFile=jit.log
jdk9+
-XX:+UnlockDiagnosticVMOptions
-Xlog:class+load=info
-XX:+LogCompilation
-XX:+PrintAssembly
-XX:+DebugNonSafepoints
-XX:LogFile=mylogfile.log
-Xint // 解释执行
-Xcomp // 编译执行
下载代码后可执行mvn clean compile exec:java启动jitwatch。mac上可能需要执行:
1
2
3
cp .../jitwatch/hsdis-amd64.dylib .../jdk1.8.0_181.jdk/Contents/Home/jre/lib/server
cp .../jitwatch/hsdis-amd64.dylib .../jdk1.8.0_181.jdk/Contents/Home/jre/bin
cp .../jitwatch/hsdis-amd64.dylib .../jdk1.8.0_181.jdk/Contents/Home/bin

