競合状態は、クリティカル セクション内で発生する可能性のある特殊な状態です。クリティカル セクションは、複数のスレッドによって実行されているコードのセクションであり、スレッドの実行順序は、クリティカル セクションの同時実行の結果に影響します。
複数のスレッドがクリティカル セクションを実行する場合、このクリティカル セクションには競合状態が含まれるため、結果が異なる場合があります。この競合状態の用語は、スレッドがクリティカル セクションを競合しており、この競合の結果がクリティカル セクションの実行結果に影響を与えるという比喩から来ています。
これは少し複雑に聞こえるかもしれないので、次のセクションで競合状態とクリティカルセクションについて詳しく説明します。
クリティカルセクション
同じアプリケーション内で複数のスレッドを実行しても、それ自体は問題を引き起こしません。複数のスレッドが同じリソースにアクセスすると、問題が発生します。たとえば、同じメモリ (変数、配列、またはオブジェクト)、システム (データベース、Web サービス)、またはファイルです。
実際、1 つ以上のスレッドがこれらのリソースに書き込むと問題が発生する可能性があります。リソースが変更されない限り、複数のスレッドが同じリソースを読み取っても安全です。
複数のスレッドが同時に実行される場合に失敗する可能性がある例を次に示します。
public class Counter { protected long count = 0; public void add(long value){ this.count = this.count + value; } }
スレッド A と B が Counter クラスの同じインスタンスの add メソッドを実行していると想像してください。オペレーティング システムがいつスレッド間を切り替えるかを知る方法はありません。 add メソッドのコードは、Java 仮想マシンによって別個のアトミック命令として実行されることはありません。代わりに、次のような一連の小さな命令セットとして実行されます:
this.count 値をメモリからレジスタに読み取ります。
レジスタに値を追加します。
レジスタの値をメモリに書き込みます。
スレッド A と B が混在して実行されると何が起こるかを観察します。
this.count = 0; A: Reads this.count into a register (0) B: Reads this.count into a register (0) B: Adds value 2 to register B: Writes register value (2) back to memory. this.count now equals 2 A: Adds value 3 to register A: Writes register value (3) back to memory. this.count now equals 3
これら 2 つのスレッドは、カウンターに 2 と 3 を追加したいと考えています。したがって、これら 2 つのスレッドの実行が終了した後の値は 5 になるはずです。ただし、2 つのスレッドは実行時にインターリーブされるため、結果は異なります。
上記の実行シーケンスの例では、両方のスレッドがメモリから値 0 を読み取ります。次に、それぞれの値 2 と 3 をその値に加算し、結果をメモリに書き戻します。 this.count に残された値は、5 の代わりに、最後のスレッドが書き込んだ値になります。上の例ではスレッド A ですが、スレッド B の場合もあります。
クリティカルセクションの競合状態
上記の例では、add メソッドのコードにクリティカル セクションが含まれています。複数のスレッドがこのクリティカル セクションを実行すると、競合状態が発生します。
より正式に言うと、2 つのスレッドが同じリソースを競合し、リソースにアクセスする順序が重要であるこの状況は、競合状態と呼ばれます。競合状態を引き起こすコードのセクションは、クリティカル セクションと呼ばれます。
競合状態の防止
競合状態の発生を防ぐには、実行されるクリティカルセクションがアトミック命令として実行されるようにする必要があります。つまり、単一のスレッドが実行すると、最初のスレッドがクリティカル セクションを離れるまで、他のスレッドは実行できなくなります。
重要なセクションでスレッド同期を使用すると、競合状態を回避できます。スレッドの同期は、Java コードの同期ロックを使用して取得できます。スレッド同期は、ロックや java.util.concurrent.atomic.AtomicInteger などのアトミック変数など、他の同期概念を使用して実現することもできます。
クリティカルセクションのスループット
小規模なクリティカルセクションの場合、クリティカルセクション全体の同期ロックが機能する場合があります。ただし、クリティカル セクションが大きい場合は、それをより小さなクリティカル セクションに分割し、複数のスレッドがそれぞれの小さなクリティカル セクションを実行できるようにする方が合理的です。共有リソースの競合を減らし、クリティカル セクション全体のスループットを向上させることができます。
これは非常に単純な Java の例です:
public class TwoSums { private int sum1 = 0; private int sum2 = 0; public void add(int val1, int val2){ synchronized(this){ this.sum1 += val1; this.sum2 += val2; } } }
add メソッドが 2 つの sum 変数に値を追加する方法に注目してください。競合状態を防ぐために、内部で実行される合計には Java 同期ロックが設定されています。この実装では、一度に 1 つのスレッドのみがこの合計を実行できます。
ただし、これら 2 つの合計変数は互いに独立しているため、次のように 2 つの別個の同期ロックに分けることができます:
public class TwoSums { private int sum1 = 0; private int sum2 = 0; public void add(int val1, int val2){ synchronized(this){ this.sum1 += val1; } synchronized(this){ this.sum2 += val2; } } }
2 つのスレッドがこの add メソッドを同時に実行できることに注意してください。 1 つのスレッドが最初の同期ロックを取得し、別のスレッドが 2 番目の同期ロックを取得します。こうすることで、スレッド間の待機時間が短縮されます。
もちろん、この例は非常に単純です。実際には、共有リソースのクリティカル セクションの分離はより複雑になる可能性があり、実行順序の可能性をより詳細に分析する必要があります。
上記は Java の競合状態と重要なセクションの内容です。さらに関連する内容については、PHP 中国語 Web サイト (m.sbmmt.com) に注目してください。