受此question的启发,我写了测试:
public class Main {
private static final long TEST_NUMBERS = 5L;
private static final long ITERATION_NUMBER = 100000L;
private static long value;
public static void main(final String [] args) throws Throwable {
for(int i=0; i<TEST_NUMBERS; i++) {
value = 0;
final Thread incrementor = new Thread(new Incrementor());
final Thread checker = new Thread(new Checker());
incrementer.start();
checker.start();
checker.join();
incrementer.join();
}
}
static class Incrementor implements Runnable {
public void run() {
for(int i=0; i<ITERATION_NUMBER; i++){
++value;
}
}
}
static class Checker implements Runnable {
public void run() {
long nonEqualsCount = 0;
for(int i=0; i<ITERATION_NUMBER; i++){
if(value != value) {
++nonEqualsCount;
}
}
System.out.println("nonEqualsCount = " + nonEqualsCount);
}
}
}
此程序以常见情况打印:
nonEqualsCount = 12; //or other non 0 value;
nonEqualsCount = 0;
nonEqualsCount = 0;
nonEqualsCount = 0;
nonEqualsCount = 0;
首先:我解释这种行为是JIT编译器的存在。 “热备”后每个线程的JIT编译器缓存值非volatile
字段。对吗?
第二:如果是对或不对,我该如何验证呢?
P.S。 - 我知道 PrintAssebly -option。
更新:环境:Windows 7 64位,JDK 1.7.0_40-b43(热点)。
答案 0 :(得分:3)
增量long
变量不是原子的(64位大)。
在条件(value != value)
中:可能发生在读取值value
之间,第一个线程可以更改值。
volatile
类型与visibility
相关联。非易失性变量值可能是陈旧的。所以你的第一个结论似乎是正确的。
答案 1 :(得分:2)
你看到的可能是JIT的神器。在它开始之前,Java字节代码被解释,这意味着在比较期间检查程序线程有很多机会被中断。
此外,由于执行了更多代码,因此CPU缓存更有可能需要刷新。
当代码由JIT优化时,它可能会插入64位操作,并且由于只执行少量代码,缓存不会再刷新到主内存,这意味着线程没有机会看到另一个人做出的改变。
答案 2 :(得分:2)
在你的程序第一次通过时,这些陈述可能是正确的:
此代码可能能够证明类型++value
(以及long
)变量的增量操作(int
)不是原子的。此外,如果未在同步块内使用,则还可以证明!=
操作不是线程安全的。但这与使用的数据类型无关。
您在第一次传递后发现更改的观察结果也是正确的,但是例如,如果您使用的是Oracle / SUN JVM,则此JIT-Complier(“Hotspot Engine”)的实现取决于技术架构正在继续。
因此很难说并且验证JIT编译器是否对此负责。尝试使用这种方法推导出JIT-Complier / Hotspot引擎的实现细节是一种非常实用的研究方法。例如,从Solaris切换到Windows时,您的观察可能会有所不同。
以下是Hotspot引擎实现细节的链接: http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html
为了产生进一步的实证结果,您可以尝试将JVM恢复为在经典模式下运行或减少JVM中的优化量(客户端模式?)。如果行为发生变化,那么这可能是理论正确性的另一个指标。
无论如何:我很好奇你的发现是什么: - )
答案 3 :(得分:2)
虽然你认为这是由JIT引起的,但它与volatile无关。
一些 JIT在运行中进行内部优化,并删除不必要的代码以加快速度,这正是这里发生的事情。 JIT确定比较value != value
始终为false并完全删除整个代码块。此外可以确定这个for循环现在是空的并且也删除了整个循环。因此,这将是最终优化的检查程序类:
public void run() {
System.out.println("nonEqualsCount = 0");
}
您可以通过测量每次传递执行此线程所需的时间来验证这一点。在第一遍可能需要一些时间才能完成,在第二阶段,println只需要几纳秒。
注意:作为一般规则,您不能指望JIT做任何事情。根据实际实施,硬件和其他因素,它可能会或可能不会优化您的代码。如果它确实优化了,结果同样无法确定,例如代码可能在慢速硬件上比在快速硬件上更早地进行优化。