【问题标题】:Why isn't the last thread interrupted?为什么最后一个线程没有中断?
【发布时间】:2018-05-10 01:44:42
【问题描述】:

我正在尝试演示一种“随时算法”——一种可以随时停止并返回其当前结果的算法。演示算法只返回 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;
    }

在主程序中,我是这样使用的:

        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();
    }
}

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

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

也就是说,当我告诉它们时,前三个线程确实被中断了,但最后一个线程在 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]");
    }

如果超时时间最多为 800,那么一切正常,它会打印出类似“806.0 [ms] 之后的超时时间”的内容。但如果超时为 900,它会打印“Timeout after 5084.0 [ms]”。

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

【问题讨论】:

  • 循环是真的在循环,还是因为某种原因卡住了?
  • @Carcigenicate 我怎么知道?
  • 也适合我。也许程序只有一个 CPU 内核可以使用,并且中断的主线程没有机会运行很长时间?在睡眠之前和中断之前打印时间戳可能会提供一些见解。
  • 很有趣,看起来它与安全点和 JIT 编译器有关。我无法用您的示例重现它,但是如果我将 {code}bestSoFar = (int)Math.pow(i, 0.3);{code} 替换为 {code}bestSoFar = i {code} 我经常看到最后一个线程不中断。也许@apangin 可以提供帮助
  • 您的系统有多少个内核?您的程序是否与任何其他进程竞争 CPU 资源?

标签: java multithreading jvm


【解决方案1】:

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

@AdamSkywalker 完全正确地假设该问题与 HotSpot HIT 编译器中的安全点消除优化有关。虽然 JDK-8154302 的 bug 看起来很相似,但实际上是另一个问题。

什么是安全点问题

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

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

    for (int i = 0; i < N; i++)

或等价物。这里 N 是int 类型的循环不变量。

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

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

    int l = 0;
    while (true) {
        if (++l == 0) ...
    }

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

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

    for (int i = 0; i < N; i += step) {
        for (int j = 0; j < step; j++) {
            // loop body
        }
        safepoint_poll();
    }

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

Thread.interrupted() 有什么问题

我很确定Thread.sleep bug 将作为Loop strip mining issue 的副本关闭。您可以使用-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

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

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

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

    for (long i=0; i<n; ++i) { ...

不会再有奇怪的延迟了。

【讨论】:

  • 很好的答案!您能否详细说明为什么线程需要到达安全点才能传播中断标志?乍一看,这似乎不是一个世界末日的操作。
  • 不,中断标志传播不需要安全点。我看不出如何从我的回答中得出这个结论。
  • 好吧,问题是为什么中断没有按预期执行,而您的回答表明原因是安全点消除。这是我所缺少的中断和安全点之间的联系。
  • @OliverGondža 关键句是:“当Thread.sleep 从本机函数返回并发现请求安全点操作时,它会停止并等待,直到长时间运行的计数循环也完成。”安全点检查基本上是 HotSpot 中每个 native method invocation 的一部分。安全点的原因是什么并不重要 - 它可能是 any random reason 与线程中断无关,例如periodic cleanup safepoint.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多