java我可以通过字节码编译日志检测指令重新排序吗?
我有下一个代码示例:
class Shared {
int x;
int y;
void increment() {
x++;
y++;
}
void check() {
if (y > x) {
System.out.println("Ooops! y > x");
}
}
}
看起来清楚吗?但当我尝试增加和签入两个线程时,主要问题发生在这里:
Shared shared = new Shared();
Thread writer = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.increment();
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.check();
}
});
writer.start();
reader.start();
您可能会注意到数据竞争(某些情况下指令会重新排序?)
1. x++;
2. y++;
现在,我知道了一些特殊的VM标志,它们可以帮助我打印JIT编译器日志(-XX:+PrintCompilation
)
...
120 181 3 Shared::increment (21 bytes)
120 182 3 Shared::check (20 bytes)
120 183 4 Shared::increment (21 bytes)
120 184 4 Shared::check (20 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
121 182 3 Shared::check (20 bytes) made not entrant
121 185 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)L (native) (static)
121 184 4 Shared::check (20 bytes) made not entrant
122 186 3 Shared::check (20 bytes)
122 187 n 0 java.lang.Object::clone (native)
122 188 4 Shared::check (20 bytes)
122 189 % 3 Main::lambda$main$0 @ 2 (19 bytes)
122 190 3 Main::lambda$main$0 (19 bytes)
123 186 3 Shared::check (20 bytes) made not entrant
...
好的,现在我可以看到增量方法的编译是如何处理的:
120 181 3 Shared::increment (21 bytes)
120 183 4 Shared::increment (21 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
我是否理解正确的,这里的重新排序是由于tiered compilation?由于increment()
-hot方法,JIT编译器会分析此信息并使用C2
服务器编译器。而且,正如我所想,以这种方式重新排列一些指令,但在某些情况下会发生优化(made not entrant
)。还是错了
此外,还有其他一些需要编译的日志:
138 182 2 Shared::increment (21 bytes)
138 184 4 Shared::increment (21 bytes)
138 182 2 Shared::increment (21 bytes) made not entrant
# 1 楼答案
这与分层编译无关。没有它,问题也会发生。让JVM只编译一个方法
check
,并查看它在C2编译代码中的外观:输出是
如您所见,
x
首先加载(第1行),然后加载y
(第2行)。在这些行之间,另一个线程可能会增加y
一个基数,从而使y
看起来大于x
在这种特殊情况下,您已经猜到了相对于原始程序顺序的加载的重新排序(在字节码中
getfield y
在getfield x
之前)。然而,正如@Andreas所提到的,这并不是程序可能中断的唯一原因。即使JIT编译器在load(x)
之前发出load(y)
,根据CPU体系结构的不同,第一个加载可能会得到一个较新的值,而第二个加载则会得到一个较旧的值,从JMM的角度来看,这是绝对正确的