【问题标题】:Is JIT reason of this behaviour?这种行为是 JIT 的原因吗?
【发布时间】:2013-11-06 08:46:24
【问题描述】:

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

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

附: - 我知道 PrintAssebly-option。

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

【问题讨论】:

标签: java multithreading jit non-volatile


【解决方案1】:

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

【讨论】:

  • 如果将 'long' 更改为 'int' 则输出未更改 =)。
  • 我没有说问题出在 long 类型上。问题在于非原子操作。递增 int 也不是原子操作。 :D
  • 是的。使用 java.util.concurrent.atomic 包中的抽象类型 AtomicIntegerAtomicLong
  • @mwhs 为什么是抽象的?
  • 术语“抽象”并不意味着它是 Java 语言中抽象类意义上的抽象,而是数据类型意义上的抽象。解释见这篇文章:en.wikipedia.org/wiki/Abstract_data_type
【解决方案2】:

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

此外,由于执行的代码越多,CPU 缓存需要刷新的可能性就越大。

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

【讨论】:

    【解决方案3】:

    在您的程序的第一次通过时,这些语句可能是正确的:

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

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

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

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

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

    无论如何:我很好奇你的发现是什么:-)

    【讨论】:

      【解决方案4】:

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

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

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

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

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

      【讨论】:

      • 这意味着当没有正确应用并发时,您(或 JIT 编译器)实际上会产生非确定性代码?
      • JIT 总是产生确定性的代码,但并行执行的时间根据定义是不确定的。因此,JIT 甚至不需要尝试。
      猜你喜欢
      • 2012-01-05
      • 1970-01-01
      • 2023-03-22
      • 1970-01-01
      • 2016-09-17
      • 1970-01-01
      • 2019-10-19
      • 2017-07-21
      • 1970-01-01
      相关资源
      最近更新 更多