7

受这个问题的启发,我编写了测试:

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字段。对的?

第二:如果第一个正确或不正确,我该如何验证这一点?

PS - 我知道PrintAssebly -option。

更新:环境:Windows 7 64bit,JDK 1.7.0_40-b43(热点)。

4

4 回答 4

3

递增long变量不是原子的(64 位大)。在条件(value != value):可能发生在读取值之间value,第一个线程可以改变值。 volatile类型与visibility. 非易失性变量值可能是陈旧的。所以你的第一个结论似乎是正确的。

于 2013-11-06T09:13:06.920 回答
2

您看到的可能是 JIT 的产物。在它开始之前,Java 字节码被解释,这意味着检查器线程在比较期间有很多机会被中断。

此外,由于执行了更多代码,CPU 缓存需要刷新的可能性更高。

当代码被 JIT 优化时,它可能会插入 64 位操作,由于只执行少量代码,缓存将不再刷新到主内存,这意味着线程没有机会看到所做的更改由另一个。

于 2013-11-06T09:59:29.157 回答
2

在您的程序的第一遍,这些语句可能是正确的:

此代码可能能够证明对类型( andalso ++value) 的变量的自增操作 ()不是原子的。此外,如果不在同步块中使用,这还可以证明该操作不是线程安全的。但这与使用的数据类型无关。longint!=

您观察到在第一次通过后发生了更改也是正确的,但例如,如果您使用的是 Oracle/SUN JVM,那么这个 JIT 编译器(“热点引擎”)的实现取决于它所运行的技术架构.

因此很难说并验证 JIT-Compiler 是否对此负责。试图用这种方法推导出 JIT-Complier/Hotspot 引擎的实现细节,是一种相当实证的研究方法。例如,从 Solaris 切换到 Windows 时,您的观察结果可能会有所不同。

这里是热点引擎实现细节的链接: http ://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html

为了产生进一步的经验结果,您可以尝试将 JVM 恢复为以经典模式运行,或者减少 JVM 中的优化量(客户端模式?)。如果行为发生变化,那么这可能是您的理论正确性的另一个指标。

不管怎样:我很好奇你的发现是什么:-)

于 2013-11-06T10:05:15.237 回答
2

虽然您说这是由 JIT 引起的,但它与 volatile 无关。

一些JIT 即时进行内部优化并删除不必要的代码以加快速度,这正是这里发生的事情。JIT 确定比较value != value始终为假,并完全删除整个代码块。它还可以确定,这个 for 循环现在是空的,并且也删除了整个循环因此,这将是最终优化的检查器类:

public void run() {
  System.out.println("nonEqualsCount = 0");
}

您可以通过测量此线程在每次传递中执行所花费的时间来验证这一点。在第一次通过时,可能需要一些时间才能完成,在第二次通过时,println 只需几纳秒。

注意:作为一般规则,您不能期望 JIT 做任何事情。根据实际实现、硬件和其他因素,它可能会或可能不会优化您的代码。如果它确实进行了优化,那么结果同样无法确定,例如,代码在慢速硬件上的优化可能比在快速硬件上更早。

于 2013-11-06T15:57:20.353 回答