次のコード スニペットは 2 つのスレッドを実行します。1 つは 1 秒ごとにログを記録する単純なタイマーで、もう 1 つは剰余演算を実行する無限ループです。
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 秒間ブロックする理由がわかりません。スレッドの優先順位やその他の設定を変更してみましたが、何も機能しませんでした。
これを修正するための提案(OS コンテキスト切り替え設定の調整を含む)があれば、お知らせください。
ベストアンサー1
ここでの説明をすべて終えた後(ピーター・ローリー) この一時停止の主な原因は、ループ内のセーフポイントに到達することがほとんどないため、JIT コンパイルされたコードの置き換えのためにすべてのスレッドを停止するのに長い時間がかかることであることがわかりました。
しかし、私はさらに深く探ってなぜwhile
セーフポイントに到達することはめったにありません。この場合、ループのバックジャンプがなぜ「安全」ではないのか、少し混乱しました。
だから私は-XX:+PrintAssembly
その栄光のすべてを召喚して助けを求める
-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel
調査の結果、ラムダC2
コンパイラーを 3 回目に再コンパイルすると、ループ内のセーフポイント ポーリングが完全に破棄されることがわかりました。
アップデート
プロファイリングの段階では、変数がi
0になることは一度もありませんでした。そのため、C2
この分岐は投機的に最適化され、ループは次のように変換されました。
for (int i = OSR_value; i != 0; i++) {
if (1 % i == 0) {
uncommon_trap();
}
}
uncommon_trap();
元々無限ループだったものが、カウンター付きの通常の有限ループに作り直されたことに注意してください。有限カウント ループでのセーフポイント ポーリングを排除する JIT 最適化により、このループでもセーフポイント ポーリングは行われませんでした。
しばらくしてi
に戻り0
、珍しい罠に陥りました。 メソッドは最適化解除され、インタープリタで実行が続行されました。 新しい知識での再コンパイル中に はC2
無限ループを認識し、コンパイルを中止しました。 メソッドの残りの部分は、適切なセーフポイントを使用してインタープリタで続行されました。
ぜひ読んでいただきたい素晴らしいブログ記事があります「セーフポイント: 意味、副作用、オーバーヘッド」によるニツァン・ワカートセーフポイントとこの特定の問題について説明します。
非常に長いカウントループでのセーフポイントの除去は問題となることが知られています。バグJDK-5014723
(感謝ウラジミール・イワノフ) はこの問題に対処します。
バグが最終的に修正されるまで、回避策は利用可能です。
- ぜひお試しください
-XX:+UseCountedLoopSafepoints
(それ意思全体的なパフォーマンスの低下を引き起こし、JVMクラッシュを引き起こす可能性があるJDK-8161147
)。これを使用すると、C2
コンパイラはバックジャンプでセーフポイントを維持し続け、元の一時停止は完全に消えます。 問題のあるメソッドのコンパイルを明示的に無効にするには、
-XX:CompileCommand='exclude,binary/class/Name,methodName'
または、手動でセーフポイントを追加してコードを書き直すこともできます。たとえば、
Thread.yield()
サイクルの最後に呼び出したり、int i
(long i
ありがとう、ニツァン・ワカート) も一時停止を修正します。