Java 线程在循环中执行余数运算会阻塞所有其他线程

标签 java multithreading

以下代码片段执行两个线程,一个是简单的计时器每秒记录一次,第二个是执行余数运算的无限循环:

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

这给出了以下结果:

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

我不明白为什么无限任务会阻塞所有其他线程 13.3 秒。我尝试更改线程优先级和其他设置,但没有任何效果。

如果您有任何解决此问题的建议(包括调整操作系统上下文切换设置),请告诉我。

最佳答案

经过这里的所有解释(感谢 Peter Lawrey),我们发现此暂停的主要原因是循环内的安全点很少到达,因此需要很长时间才能停止所有线程以进行 JIT 编译的代码替换.

但我决定深入了解为什么很少达到安全点。我发现在这种情况下为什么 while 循环的向后跳转不是“安全”的,我觉得有点困惑。

所以我召唤 -XX:+PrintAssembly 来提供帮助

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

经过一番调查,我发现在第三次重新编译 lambda C2 编译器后,完全丢弃了循环内的安全点轮询。

更新

在分析阶段变量 i 从未被视为等于 0。这就是为什么 C2 推测性地优化了这个分支,以便将循环转换为类似的东西

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

请注意,最初的无限循环被 reshape 为带有计数器的常规有限循环!由于 JIT 优化消除了有限计数循环中的安全点轮询,因此该循环中也没有安全点轮询。

一段时间后,i 回绕到 0,并采取了不常见的陷阱。该方法被取消优化并在解释器中继续执行。在用新知识重新编译期间,C2 识别出无限循环并放弃编译。该方法的其余部分在解释器中进行,并带有适当的安全点。

有一篇很棒的必读博文 "Safepoints: Meaning, Side Effects and Overheads"Nitsan Wakart涵盖安全点和这个特定问题。

众所周知,在很长的计数循环中消除安全点是一个问题。错误 JDK-5014723 (感谢 Vladimir Ivanov )解决了这个问题。

在错误最终修复之前,解决方法是可用的。

  1. 您可以尝试使用-XX:+UseCountedLoopSafepoints (它导致整体性能下降并且可能导致 JVM 崩溃 JDK-8161147)。使用后C2编译器继续在后面的跳转处保持安全点,原来的暂停完全消失了。
  2. 您可以使用
    显式禁用编译有问题的方法 -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. 或者您可以通过手动添加安全点来重写您的代码。例如 Thread.yield() 在循环结束时调用,甚至将 int i 更改为 long i (感谢 Nitsan Wakart )将还修复了暂停。

关于Java 线程在循环中执行余数运算会阻塞所有其他线程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39298474/

相关文章:

java - 是否可以在 Libgdx 中编写 C/C++ 代码?

c++ - C++ 的非线程异步 IO 简介?

c# - 在 WPF 中安全访问 UI(主)线程

c# - .NET 中是否有线程锁的任何低级别度量/日志记录?

ios - Async_Dispatch 线程

java - 来自 null 变量的 NullPointerException 错误。当 ListView 变空时

java - JPA 一对多关系映射

java - 在虚拟机上运行 selenium grid 的问题

c - fifo 循环队列中的 pthread_cond_wait 死锁

java - 如何在 Jersey REST Web 服务中返回数组?