在循环中执行余数操作的Java线程会阻塞所有其他线程
下面的代码片断执行两个线程,一个是每秒钟一个简单的计时器,第二个是执行余数操作的无限循环:
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 cmcconcurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=13331 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1006 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1004
我不明白为什么无限的任务阻塞所有其他线程13.3秒。 我试图改变线程优先级和其他设置,没有任何工作。
如果你有任何build议来解决这个问题(包括调整操作系统上下文切换设置)请让我知道。
在所有的解释之后(感谢Peter Lawrey ),我们发现这个暂停的主要来源是循环内的safepoint很less到达,所以需要很长时间才能停止JIT编译代码replace的所有线程。
但是我决定更深入地寻找为什么很less达到安全点。 在这种情况下,我发现为什么while
循环的回跳不“安全”,有点令人困惑。
所以我就-XX:+PrintAssembly
在其所有的荣耀中提供帮助
-XX:+UnlockDiagnosticVMOptions \ -XX:+TraceClassLoading \ -XX:+DebugNonSafepoints \ -XX:+PrintCompilation \ -XX:+PrintGCDetails \ -XX:+PrintStubCode \ -XX:+PrintAssembly \ -XX:PrintAssemblyOptions=-Mintel
经过一番调查,我发现经过第三次lambda C2
编译器的编译后,完全丢掉了循环内的安全点轮询。
UPDATE
在剖析阶段,variablesi
从来没有被看作等于0.这就是为什么C2
推测性地优化了这个分支,以便循环被转换成类似
for (int i = OSR_value; i != 0; i++) { if (1 % i == 0) { uncommon_trap(); } } uncommon_trap();
请注意,最初的无限循环被重新定义为一个有计数器的常规有限循环! 由于JIT优化以消除有限计数循环中的安全点轮询,所以在该循环中也没有安全点轮询。
过了一段时间, i
回到了0
,并采取了不寻常的陷阱。 该方法被解除优化,并继续在解释程序中执行。 在重新编译新知识的过程中, C2
认识到了无限循环,放弃了编译。 该方法的其余部分在解释器中以适当的安全点进行。
Nitsan Wakart有一个很好的必读博客文章“Safepoints:意义,副作用和开销” , 内容涵盖安全点和这个特定问题。
已知在非常长的计数循环中消除安全点是一个问题。 错误JDK-5014723
(感谢弗拉基米尔·伊万诺夫 )解决了这个问题。
解决方法是可用的,直到错误最终修复。
- 您可以尝试使用
-XX:+UseCountedLoopSafepoints
(这会导致整体性能-XX:+UseCountedLoopSafepoints
,并可能导致JVM崩溃JDK-8161147
)。 使用之后,C2
编译器继续在后跳保持安全点,原始暂停完全消失。 -
您可以通过使用显式禁用编译有问题的方法
-XX:CompileCommand='exclude,binary/class/Name,methodName'
-
或者您可以通过手动添加安全点来重写代码。 例如
Thread.yield()
在循环结束时调用,甚至改变int i
long i
(long i
感谢, Nitsan Wakart )也将修复暂停。
简而言之,除了达到i == 0
时,您所拥有的循环内部没有任何安全点。 当这个方法被编译并且触发代码被replace时,它需要把所有的线程放到一个安全的位置,但是这需要很长的时间,不仅locking运行代码的线程,而且lockingJVM中的所有线程。
我添加了下面的命令行选项。
-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation
我也修改了代码,以使用浮点,这似乎需要更长的时间。
boolean b = 1.0 / i == 0;
而我在输出中看到的是
timeElapsed=100 Application time: 0.9560686 seconds 41423 280 % 4 TestBlockingThread::lambda$main$0 @ -2 (27 bytes) made not entrant Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds Application time: 0.0000219 seconds Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds 41424 281 % 3 TestBlockingThread::lambda$main$0 @ 2 (27 bytes) timeElapsed=40473 41425 282 % 4 TestBlockingThread::lambda$main$0 @ 2 (27 bytes) 41426 281 % 3 TestBlockingThread::lambda$main$0 @ -2 (27 bytes) made not entrant timeElapsed=100
注意:对于要replace的代码,线程必须在安全的地方停下来。 然而,在这里看来,这样一个安全点很less达到(可能只有当i == 0
改变任务
Runnable task = () -> { for (int i = 1; i != 0 ; i++) { boolean b = 1.0 / i == 0; } };
我看到类似的延迟。
timeElapsed=100 Application time: 0.9587419 seconds 39044 280 % 4 TestBlockingThread::lambda$main$0 @ -2 (28 bytes) made not entrant Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds Application time: 0.0000087 seconds Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds timeElapsed=38100 timeElapsed=100
仔细地将代码添加到循环中会得到更长的延迟。
for (int i = 1; i != 0 ; i++) { boolean b = 1.0 / i / i == 0; }
得到
Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds
但是,将代码更改为使用始终具有安全点的本地方法(如果它不是内在的)
for (int i = 1; i != 0 ; i++) { boolean b = Math.cos(1.0 / i) == 0; }
版画
Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds
注意:将if (Thread.currentThread().isInterrupted()) { ... }
添加到循环会添加一个安全点。
注意:这发生在一台16核心的机器上,所以没有CPU资源。
find答案为什么 。 他们被称为安全点(safepoint),最为人所知的就是因为GC而发生的停止世界(Stop-The-World)。
看到这篇文章: logging停止在JVM的世界暂停
不同的事件会导致JVM暂停所有的应用程序线程。 这种暂停被称为停止世界(STW)暂停。 STW暂停被触发的最常见原因是垃圾收集(在github中的例子),但是不同的JIT动作 (例子),有偏差的locking撤销(例子),某些JVMTI操作等等也要求应用程序停止。
应用程序线程可以安全停止的点称为惊喜安全点 。 这个术语也经常用来指代所有的STW暂停。
GC日志启用或多或less是常见的。 但是,这不会捕获所有安全点上的信息。 为了实现这一切,请使用这些JVM选项:
-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime
如果您想明确指出GC的命名,请不要惊慌 – 打开这些选项会logging所有安全点,而不仅仅是垃圾收集暂停。 如果你用上面指定的标志运行下面的例子(在github中的源代码)。
阅读HotSpot术语表 ,它定义了这一点:
还原点
程序执行过程中的一个点,所有GC根已知,所有堆对象内容一致。 从全局angular度来看,所有的线程都必须在GC运行之前在一个安全点上进行阻塞。 (作为一种特殊情况,运行JNI代码的线程可以继续运行,因为它们只使用句柄。在一个安全点期间,它们必须阻塞,而不是加载句柄的内容。)从本地的angular度来看,一个安全点是一个识别点在执行线程可能阻塞GC的代码块中。 大多数通话网站都是符合安全标准的。 在每个安全点都有强大的不variables,在非安全点可能会被忽视。 编译的Java代码和C / C ++代码都在安全点之间进行了优化,但在安全点之间进行了优化。 JIT编译器在每个安全点发出一个GC映射。 VM中的C / C ++代码使用风格化的基于macros的约定(例如TRAPS)来标记潜在的安全点。
运行上面提到的标志,我得到这个输出:
Application time: 0.9668750 seconds Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds timeElapsed=1015 Application time: 1.0148568 seconds Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds timeElapsed=1015 timeElapsed=1014 Application time: 2.0453971 seconds Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds timeElapsed=11732 Application time: 1.0149263 seconds Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds timeElapsed=1015
注意第三个STW事件:
总时间停止: 10.7951187秒
停止线程花费: 10.7950774秒
JIT本身几乎没有时间,但是一旦JVM决定执行JIT编译,它就进入STW模式,但是由于要编译的代码(无限循环)没有调用站点 ,所以没有达到任何安全点。
当JIT最终放弃等待并结束代码处于无限循环时,STW结束。
在遵循评论线程和我自己的一些testing之后,我认为暂停是由JIT编译器引起的。 为什么JIT编译器需要这么长的时间已经超出了我的debugging能力。
但是,既然你只是问如何防止这个,我有一个解决scheme:
把你的无限循环放到可以从JIT编译器中排除的方法中
public class TestBlockingThread { private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName()); public static final void main(String[] args) throws InterruptedException { Runnable task = () -> { infLoop(); }; new Thread(new LogTimer()).start(); Thread.sleep(2000); new Thread(task).start(); } private static void infLoop() { int i = 0; while (true) { i++; if (i != 0) { boolean b = 1 % i == 0; } } }
用这个VM参数运行你的程序:
-XX:CompileCommand = exclude,PACKAGE.TestBlockingThread :: infLoop(用你的包信息replacePACKAGE)
你应该得到这样的消息来指示何时该方法将被JIT编译:
###排除compile:static blocking.TestBlockingThread :: infLoop
你可能会注意到我把这个类放到一个叫阻塞的包里