以下代码片段执行两个线程,一个是每秒记录一次的简单计时器,第二个是执行余数操作的无限循环:
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秒 . 我试图改变线程优先级和其他设置,没有任何效果 .
如果您有任何解决此问题的建议(包括调整操作系统上下文切换设置),请告诉我 .
4 回答
在这里的所有解释之后(感谢Peter Lawrey),我们发现这个暂停的主要来源是很少到达循环内部的安全点,因此需要很长时间才能停止所有线程以进行JIT编译的代码替换 .
但我决定更深入地发现 why 很少达到安全点 . 我觉得有点混乱为什么在这种情况下
while
循环的后跳不是"safe" .所以我尽其所能地召唤
-XX:+PrintAssembly
来帮助他们经过一番调查后,我发现在第三次重新编译lambda
C2
编译器后,完全抛弃了内部循环中的安全点轮询 .UPDATE
在分析阶段,变量
i
从未被视为等于0.这就是为什么C2
推测性地优化了这个分支,以便循环转换为类似的东西请注意,最初的无限循环被重新变换为带有计数器的常规有限循环!由于JIT优化以消除有限计数循环中的安全点轮询,因此在该循环中也没有安全点轮询 .
过了一段时间,
i
回到0
,并且采取了不常见的陷阱 . 该方法被去优化并在解释器中继续执行 . 在使用新知识重新编译期间C2
识别出无限循环并放弃编译 . 该方法的其余部分在解释器中以适当的安全点进行 ."Safepoints: Meaning, Side Effects and Overheads"有一篇很棒的必读博客文章,涵盖了安全点和这个特殊问题 .
已知非常长的计数循环中的安全点消除是一个问题 . 错误JDK-5014723(感谢Vladimir Ivanov)解决了这个问题 .
在最终修复错误之前,可以使用解决方法 .
您可以尝试使用-XX:+UseCountedLoopSafepoints(这将导致整体性能损失和 may lead to JVM crash JDK-8161147) . 使用后
C2
编译器继续保持后跳的安全点,原始暂停完全消失 .您可以使用显式禁用有问题的方法的编译
-XX:CompileCommand='exclude,binary/class/Name,methodName'
或者您可以通过手动添加安全点来重写代码 . 例如
Thread.yield()
在循环结束时调用甚至将int i
更改为long i
(谢谢,Nitsan Wakart)也将修复暂停 .简而言之,除非达到
i == 0
,否则您拥有的循环内部没有安全点 . 编译此方法并触发要替换的代码时,需要将所有线程都置于安全点,但这需要很长时间,不仅要锁定运行代码的线程,还要锁定JVM中的所有线程 .我添加了以下命令行选项 .
我还修改了代码以使用浮点,这似乎需要更长的时间 .
我在输出中看到的是
注意:对于要替换的代码,必须在安全点停止线程 . 然而,在这里似乎很少达到这样一个安全点(可能只有在
i == 0
将任务更改为我看到类似的延迟 .
仔细地将代码添加到循环中会导致更长的延迟 .
得到
但是,更改代码以使用始终具有安全点的本机方法(如果它不是内在的)
版画
注意:将
if (Thread.currentThread().isInterrupted()) { ... }
添加到循环会添加一个安全点 .注意:这发生在16核计算机上,因此不缺CPU资源 .
找到了原因的答案 . 它们被称为安全点,最出名的是由于GC而发生的Stop-The-World .
看到这篇文章:Logging stop-the-world pauses in JVM
阅读HotSpot Glossary of Terms,它定义了这个:
运行上面提到的标志,我得到这个输出:
注意第三个STW事件:
停止总时间:10.7951187秒
停止线程: 10.7950774 seconds
JIT本身几乎没有时间,但是一旦JVM决定执行JIT编译,它就进入了STW模式,但是由于要编译的代码(无限循环)没有调用站点,所以没有达到安全点 .
当JIT最终放弃等待并且结束代码处于无限循环时,STW结束 .
在我自己跟踪注释线程和一些测试之后,我认为暂停是由JIT编译器引起的 . 为什么JIT编译器花了这么长时间超出了我的调试能力 .
但是,由于您只询问如何防止这种情况,我有一个解决方案:
将无限循环拉入可以从JIT编译器中排除的方法
使用此VM参数运行程序:
-XX:CompileCommand = exclude,PACKAGE.TestBlockingThread :: infLoop(用包信息替换PACKAGE)
您应该得到这样的消息,以指示该方法何时进行JIT编译:
###排除编译:static blocking.TestBlockingThread :: infLoop
您可能会注意到我将该类放入一个名为blocking的包中