チュートリアル: 並行性の問題を検出する
同時実行関連のバグはランダムな性質を持つため、シングルスレッドアプリケーションのバグよりも扱いが難しいことがよくあります。 アプリは 1,000 回問題なく実行されても、明らかな理由もなく予期せず失敗することがあります。
このチュートリアルでは、マルチスレッドアプリのデバッグと分析のコア原則を示すコード例を分析します。
問題
同時実行性に関連するバグの一般的な例は競合状態です。 これは、共有データが複数のスレッドによって適切に同期されずに同時に変更された場合に発生します。 このようなコードは、2 つのスレッドの読み取りと書き込みが重複しない限り、正常に動作する可能性があります。

重複は非常にまれであり、コードに欠陥がないと思わせる可能性があります。 ただし、スレッド操作が重複すると、データが破損します。

これを考慮しないと、特に単一の読み取りと書き込みよりも複雑な処理を扱う場合には、スレッドが同時にデータを操作しないという保証はありません。 幸い、Java には、一度に 1 つのスレッドだけがデータを処理するようにする同期メカニズムが組み込まれています。
次のコードを考えてみましょう:
addIfAbsent メソッドは、リストに特定の要素が含まれているかを確認し、含まれていない場合は、その要素を追加します。 このメソッドを異なるスレッドから 2 回呼び出します。 どちらの場合も同じ整数値 (17) を渡し、ガード条件 (!a.contains(x)) により、そのメソッドを呼び出す最初のスレッドのみが値を追加できるようになります。 SynchronizedList を使用すると、競合状態から 保護されることになっています。 最後に、 System.out.println(a) ステートメントでリストの内容を出力します。
このコードを長期間使用すると、予期しない結果が生じる場合があります。
原因を見つけるために、コードがどのように動作するかを調べて、競合状態を本当に防ぐことができたかどうかを確認しましょう。
バグを再現する
IntelliJ IDEA デバッガーを使用すると、アプリケーションのマルチスレッド設計をテストし、アプリケーション全体ではなく個々のスレッドの実行を制御することで同時実行関連のバグを再現できます。
リストに要素を追加するステートメントにブレークポイントを設定します。

ブレークポイントは、ヒットしたスレッドのみを中断するように構成します。 これにより、両方のスレッドが同じ行で中断されます。 これを行うには、ブレークポイントを右クリックしてから スレッド をクリックします。

mainメソッドの近くにある 実行 ボタンをクリックし、 デバッグ を選択して、デバッグセッションを開始します。
プログラムを実行すると、両方のスレッドが
addIfAbsentメソッドで個別に中断されます。 これで、 スレッド タブでスレッドを切り替えて、各スレッドの実行を制御できるようになります。この時点で、両方のスレッドはリストに
17が含まれていないことを確認し、リストに番号を追加する準備ができています。スレッド タブで、
Thread-0に切り替えます。
F9 を押すか、 デバッグ ツールウィンドウの左側にある
をクリックして、スレッドを再開します。
Thread-0を再開すると、17をリストに追加し、終了します。 その後、デバッガーは自動的にメインスレッドに戻ります。メインスレッドを再開して、残りのステートメントを実行してから終了します。
コンソール タブでプログラム出力を確認します。

出力 [17, 17] は、誤って設定されたガード条件と同期をバイパスして、2 つのスレッドが同じ値を追加できたことを示しています。 デバッガーを使用してイベントの順序を再現したところ、競合状態が存在することがわかり、アプローチを修正する必要があることがわかりました。
プログラムを修正する
先ほど見たように、 SynchronizedList だけを使用するのは不十分でした。 一度に 1 つのスレッドだけがリストを変更するようにしました。 ただし、 if (!a.contains(x)) のチェックと a.add(x) の変更はアトミック操作ではないことを考慮に入れる必要がありました。 このため、両方のスレッドが同時に条件を評価し、コードブロックに入ることができました。
同期ブロックで条件をラップして、コードを修正しましょう。
修正されたコードを使用して手順を繰り返し、問題がなくなったことを確認できます。