static int algorithm(int n) { int bestSoFar = 0; for (int i=0; i<n; ++i) { if (Thread.interrupted()) break; bestSoFar = (int)Math.pow(i,0.3); } return bestSoFar; }
在主程序中,我使用它像这样:
Runnable task = () -> { Instant start = Instant.now(); int bestSoFar = algorithm(1000000000); double durationInMillis = Duration.between(start,Instant.now()).toMillis(); System.out.println("after "+durationInMillis+" ms,the result is "+bestSoFar); }; Thread t = new Thread(task); t.start(); Thread.sleep(1); t.interrupt(); t = new Thread(task); t.start(); Thread.sleep(10); t.interrupt(); t = new Thread(task); t.start(); Thread.sleep(100); t.interrupt(); t = new Thread(task); t.start(); Thread.sleep(1000); t.interrupt(); } }
当我运行此程序时,我得到以下输入:
after 0.0 ms,the result is 7 after 10.0 ms,the result is 36 after 100.0 ms,the result is 85 after 21952.0 ms,the result is 501
即,当我告诉他们时,前三个线程确实被中断,但是最后一个线程在1秒后没有被中断 – 它继续工作将近22秒.为什么会这样?
编辑:我使用Future.get获得类似的结果超时.在这段代码中:
Instant start = Instant.now(); ExecutorService executor = Executors.newCachedThreadPool(); Future<?> future = executor.submit(task); try { future.get(800,TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); double durationInMillis = Duration.between(start,Instant.now()).toMillis(); System.out.println("Timeout after "+durationInMillis+" [ms]"); }
如果超时最多为800,则一切正常,并打印出类似“806.0 [ms]后超时”的内容.但如果超时为900,则打印“5084.0 [ms]后超时”.
编辑2:我的电脑有4个核心.该程序在Open JDK 8上运行.
解决方法
@AdamSkywalker绝对正确地认为该问题与HotSpot HIT编译器中的安全点消除优化有关.虽然bug JDK-8154302看起来很相似,但事实上它是一个不同的问题.
什么是安全点问题
Safepoint是用于停止应用程序线程以执行需要stop-the-world pause的操作的JVM机制.HotSpot中的安全点是协作的,即应用程序线程定期检查它们是否需要停止.此检查通常发生在方法出口和内部循环中.
当然,这项检查不是免费的.因此,出于性能原因,JVM尝试消除冗余安全点轮询.其中一种优化是从计算的循环中删除安全点轮询 – 表单的循环
for (int i = 0; i < N; i++)
或同等学历.这里N是int类型的循环不变量.
通常这些循环是短暂运行的,但在某些情况下它们可能需要很长时间,例如,当N = 2_000_000_000时.安全点操作要求停止所有Java线程(不包括运行本机方法的线程).也就是说,单个长时间运行的计数循环可能会延迟整个安全点操作,而所有其他线程将等待此循环停止.
这正是07000中发生的事情.请注意
int l = 0; while (true) { if (++l == 0) ... }
只是表达232次迭代的计数循环的另一种方式.当Thread.sleep从本机函数返回并发现请求安全点操作时,它会停止并等待,直到长时间运行的计数循环也完成.这就是怪异延误的来源.
有一个任务来解决这个问题 – JDK-8186027.想法是将一个长循环分成两部分:
for (int i = 0; i < N; i += step) { for (int j = 0; j < step; j++) { // loop body } safepoint_poll(); }
它尚未实现,但修复程序针对的是JDK 10.同时还有一个解决方法:JVM标志-XX:UseCountedLoopSafepoints也将强制安全点检查计数循环内部.
Thread.interrupted()有什么问题
我很确定Thread.sleep bug将作为Loop strip mining issue的副本关闭.您可以使用-XX:UseCountedLoopSafepoints选项验证此错误消失.
不幸的是,这个选项对原始问题没有帮助.我抓住原始问题中的算法挂起并查看正在gdb下执行的代码的那一刻:
loop_begin: 0x00002aaaabe903d0: mov %ecx,%r11d 0x00002aaaabe903d3: inc %r11d ; i++ 0x00002aaaabe903d6: cmp %ebp,%r11d ; if (i >= n) 0x00002aaaabe903d9: jge 0x2aaaabe90413 ; break; 0x00002aaaabe903db: mov %ecx,%r8d 0x00002aaaabe903de: mov %r11d,%ecx 0x00002aaaabe903e1: mov 0x1d0(%r15),%rsi ; rsi = Thread.current(); 0x00002aaaabe903e8: mov 0x1d0(%r15),%r10 ; r10 = Thread.current(); 0x00002aaaabe903ef: cmp %rsi,%r10 ; if (rsi != r10) 0x00002aaaabe903f2: jne 0x2aaaabe903b9 ; goto slow_path; 0x00002aaaabe903f4: mov 0x128(%r15),%r10 ; r10 = current_os_thread(); 0x00002aaaabe903fb: mov 0x14(%r10),%r11d ; isInterrupted = r10.interrupt_flag; 0x00002aaaabe903ff: test %r11d,%r11d ; if (!isInterrupted) 0x00002aaaabe90402: je 0x2aaaabe903d0 ; goto loop_begin
这就是编译算法方法中的循环的方法.这里没有安全点调查,即使设置了-XX:UseCountedLoopSafepoints也是如此.
看起来安全点检查被错误地消除了,因为Thread.isInterrupted调用本应检查安全点本身.但是,Thread.isInterrupted是HotSpot的内在方法.这意味着没有真正的本机方法调用,但是JIT用一系列机器指令替换了对Thread.isInterrupted的调用,里面没有安全点检查.
我很快就会向Oracle报告这个错误.同时,解决方法是将循环计数器的类型从int更改为long.如果你将循环重写为
for (long i=0; i<n; ++i) { ...
不再有奇怪的延迟了.