为什么最后一个线程没有中断?

Ere*_*evi 7 java multithreading jvm

我试图演示一个"随时算法" - 一种可以随时停止并返回其当前结果的算法.演示算法只返回i的一些数学函数,其中i正在增加.它会查看它是否被中断,如果是,则返回当前值:

    static int algorithm(int n) {
        int bestSoFar = 0;
        for (int i=0; i<n; ++i) {
            if (Thread.interrupted())
                break;
            bestSoFar = (int)Math.pow(i, 0.3);
        }
        return bestSoFar;
    }
Run Code Online (Sandbox Code Playgroud)

在主程序中,我使用它像这样:

        Runnable task = () -> {
            Instant start = Instant.now();
            int bestSoFar = algorithm(1000000000);
            double durationInMillis = Duration.between(start, Instant.now()).toMillis();
            System.out.println("after "+durationInMillis+" ms, the result is "+bestSoFar);
        };

        Thread t = new Thread(task);
        t.start();
        Thread.sleep(1);
        t.interrupt();

        t = new Thread(task);
        t.start();
        Thread.sleep(10);
        t.interrupt();

        t = new Thread(task);
        t.start();
        Thread.sleep(100);
        t.interrupt();

        t = new Thread(task);
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行此程序时,我得到以下输入:

after 0.0 ms, the result is 7
after 10.0 ms, the result is 36
after 100.0 ms, the result is 85
after 21952.0 ms, the result is 501
Run Code Online (Sandbox Code Playgroud)

也就是说,当我告诉他们时,前三个线程确实被打断了,但是最后一个线程在1秒后没有中断 - 它继续工作了将近22秒.为什么会这样?

编辑:我使用Future.get和超时获得类似的结果.在这段代码中:

    Instant start = Instant.now();
    ExecutorService executor = Executors.newCachedThreadPool();
    Future<?> future = executor.submit(task);
    try {
        future.get(800, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
        future.cancel(true);
        double durationInMillis = Duration.between(start, Instant.now()).toMillis();
        System.out.println("Timeout after "+durationInMillis+" [ms]");
    }
Run Code Online (Sandbox Code Playgroud)

如果超时最多为800,则一切正常,并打印出类似"806.0 [ms]后的超时".但如果超时为900,则打印"5084.0 [ms]后超时".

编辑2:我的电脑有4个核心.程序在Open JDK 8上运行.

apa*_*gin 4

我可以确认这是一个 HotSpot JVM 错误。这是我对问题的初步分析。

@AdamSkywalker 的假设是绝对正确的,即该问题与 HotSpot HIT 编译器中的安全点消除优化有关。虽然错误JDK-8154302看起来很相似,但实际上它是一个不同的问题。

什么是安全点问题

Safepoint是一种 JVM 机制,用于停止应用程序线程以执行需要停止世界暂停的操作。HotSpot 中的安全点是协作的,即应用程序线程定期检查它们是否需要停止。此检查通常发生在方法出口和循环内部。

当然,这项检查并不是免费的。因此,出于性能原因,JVM 尝试消除冗余的安全点轮询。其中一种优化是从计数循环中删除安全点轮询 - 形式的循环

    for (int i = 0; i < N; i++)
Run Code Online (Sandbox Code Playgroud)

或同等学历。这里 N 是类型的循环不变量int

通常这些循环运行时间较短,但在某些情况下它们可能需要很长时间,例如当 N = 2_000_000_000 时。安全点操作要求停止所有Java 线程(不包括运行本机方法的线程)。也就是说,单个长时间运行的计数循环可能会延迟整个安全点操作,并且所有其他线程将等待该循环停止。

这正是JDK-8154302中发生的情况。注意

    int l = 0;
    while (true) {
        if (++l == 0) ...
    }
Run Code Online (Sandbox Code Playgroud)

只是表达 2 32 次迭代的计数循环的另一种方式。当Thread.sleep从本机函数返回并发现请求安全点操作时,它会停止并等待,直到长时间运行的计数循环也完成。这就是奇怪的延迟的来源。

有一个任务可以解决这个问题 - JDK-8186027。这个想法是将一个长循环分成两部分:

    for (int i = 0; i < N; i += step) {
        for (int j = 0; j < step; j++) {
            // loop body
        }
        safepoint_poll();
    }
Run Code Online (Sandbox Code Playgroud)

它尚未实现,但修复针对的是 JDK 10。同时有一个解决方法:JVM 标志-XX:+UseCountedLoopSafepoints也会强制在计数循环内进行安全点检查。

Thread.interrupted() 有什么问题

我非常确定Thread.sleep 错误将作为循环条带挖掘问题的重复项而被关闭。您可以验证该错误是否会通过选项消失-XX:+UseCountedLoopSafepoints

不幸的是,这个选项对解决原来的问题没有帮助。我抓住了原始问题挂起的那一刻algorithm,并查看了在 gdb 下执行的代码:

loop_begin:
  0x00002aaaabe903d0:  mov    %ecx,%r11d
  0x00002aaaabe903d3:  inc    %r11d             ; i++
  0x00002aaaabe903d6:  cmp    %ebp,%r11d        ; if (i >= n)
  0x00002aaaabe903d9:  jge    0x2aaaabe90413    ;     break;
  0x00002aaaabe903db:  mov    %ecx,%r8d
  0x00002aaaabe903de:  mov    %r11d,%ecx
  0x00002aaaabe903e1:  mov    0x1d0(%r15),%rsi  ; rsi = Thread.current();
  0x00002aaaabe903e8:  mov    0x1d0(%r15),%r10  ; r10 = Thread.current();
  0x00002aaaabe903ef:  cmp    %rsi,%r10         ; if (rsi != r10)
  0x00002aaaabe903f2:  jne    0x2aaaabe903b9    ;     goto slow_path;
  0x00002aaaabe903f4:  mov    0x128(%r15),%r10  ; r10 = current_os_thread();
  0x00002aaaabe903fb:  mov    0x14(%r10),%r11d  ; isInterrupted = r10.interrupt_flag;
  0x00002aaaabe903ff:  test   %r11d,%r11d       ; if (!isInterrupted)
  0x00002aaaabe90402:  je     0x2aaaabe903d0    ;     goto loop_begin
Run Code Online (Sandbox Code Playgroud)

这就是循环方法的algorithm编译方式。这里没有安全点轮询,即使-XX:+UseCountedLoopSafepoints设置了也是如此。

看起来安全点检查被错误地消除了,因为Thread.isInterrupted应该检查安全点本身的调用。然而,Thread.isInterrupted是HotSpot内在的方法。这意味着没有真正的本机方法调用,但 JITThread.isInterrupted用一系列机器指令替换了调用,内部没有安全点检查。

我很快就会向 Oracle 报告该错误。同时,解决方法是将循环计数器的类型从 更改intlong。如果将循环重写为

    for (long i=0; i<n; ++i) { ...
Run Code Online (Sandbox Code Playgroud)

不会再有奇怪的延误了。