以下代码片段执行两个线程,一个是简单的计时器每秒记录一次,第二个是执行余数运算的无限循环:
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 )解决了这个问题。
在错误最终修复之前,解决方法是可用的。
- 您可以尝试使用
-XX:+UseCountedLoopSafepoints
(它将导致整体性能下降并且可能导致 JVM 崩溃JDK-8161147
)。使用后C2
编译器继续在后面的跳转处保持安全点,原来的暂停完全消失了。 您可以使用
显式禁用编译有问题的方法-XX:CompileCommand='exclude,binary/class/Name,methodName'
或者您可以通过手动添加安全点来重写您的代码。例如
Thread.yield()
在循环结束时调用,甚至将int i
更改为long i
(感谢 Nitsan Wakart )将还修复了暂停。
关于Java 线程在循环中执行余数运算会阻塞所有其他线程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39298474/