- 以前、私のシステムのCPU使用率が3,200%に達し、32コアすべてが埋まっていた
- Java 17ランタイムを使用しており、スレッドダンプでCPU時間を確認してCPU時間順に並べると、よく似たスレッドが多数見つかった
- 問題のコードを分析
- スタックトレースを通じて
BusinessLogic クラスの29行目を確認
- 該当コードは
unrelatedObjects リストを反復しながら、relatedObject の値を treeMap に挿入する形になっていた
- これはループ内部で
unrelatedObject を使用していない非効率なコードだった
コード修正とテスト
- 不要なループを削除し、
treeMap.put(relatedObject.a(), relatedObject.b()); の1行に修正
- 修正前後で単体テストを実施したが、問題は再現できなかった
treeMap と unrelatedObjects のサイズがそれぞれ1,000,000件を超えていても問題は発生しなかった
問題の原因を発見
treeMap は複数スレッドから同時にアクセスされていたが、同期化されていなかった
- これは複数スレッドが
TreeMap を同時に変更したことで発生した問題だった
実験による問題の再現
- 複数スレッドが共有された
TreeMap をランダムに更新する実験を行った
try-catch ブロックを使い、NullPointerException を無視するように設定
- 実験の結果、CPU使用率が500%まで上昇する現象を確認した
結論
- 同期化されていない
TreeMap の同時変更は、深刻な性能問題を引き起こす可能性がある
- このような問題を防ぐため、
TreeMap を同期化するか、ConcurrentMap のようなスレッドセーフなコレクションを使用することが推奨される
1件のコメント
Hacker Newsの意見
レースコンディションはデータ破損やデッドロックを引き起こすものだと思っていたが、性能問題も引き起こしうるとは考えていなかった。データが無限ループを生成する形で破損することがある
複数スレッドが動作するコードでは、すべてのオブジェクトを不変にし、不変にできないオブジェクトは小さく自己完結した厳密に管理された区画に閉じ込めるのが唯一確実な戦略
「ssh にほとんど接続できなかった」という記述で、大学院時代に Sun UltraSparc 170 を使っていた状況を思い出した
コードは単に次のようにまで簡略化できる
無限ループを引き起こす別の方法として、一貫した全順序を実装していない <i>Comparator</i> または <i>Comparable</i> 実装を使うケースがある
増加カウンタを使ってサイクルを検出し、ツリーの深さやコレクションサイズを超えたら例外を投げる方法を検討できる
Java でスレッドセーフでないオブジェクトに対して並行処理を行うと、最も興味深いバグが生まれる
保護されていない TreeMap が 3,200% の使用率を引き起こしうるのかという疑問がある
著者は Poison Pill の一種を発見した。これはイベントソーシングシステムでより一般的で、遭遇するものすべてを殺すメッセージのこと
スレッド内の例外は絶対的な問題