在循环中执行余数操作的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 (感谢弗拉基米尔·伊万诺夫 )解决了这个问题。

解决方法是可用的,直到错误最终修复。

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

  3. 或者您可以通过手动添加安全点来重写代码。 例如Thread.yield()在循环结束时调用,甚至改变int i long ilong 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
你可能会注意到我把这个类放到一个叫阻塞的包里