Java マルチスレッド テクノロジは、常に Java プログラマーに必要なコア スキルの 1 つです。Java マルチスレッド プログラミングでは、データの一貫性とセキュリティを確保するために、多くの場合、ロック メカニズムを使用して、複数のスレッドが同じ共有リソースを同時に変更できないようにする必要があります。ロックは同時アクセス制御を実現するメカニズムです。複数のスレッドが共有リソースに共同でアクセスする場合、悲観的ロックが最も一般的な方法です。ただし、同時実行性が高いシナリオでは、グローバル ロックによって引き起こされる同時実行性のブロック問題も避けられません。この問題を解決するために、人々は楽観的ロックの概念を導入しました。この記事では、Java マルチスレッドにおける楽観的ロックと悲観的ロックについて詳しく説明します。
1. 悲観的ロックとは何ですか
悲観的ロックは、マルチスレッド同時実行制御のメカニズムです。悲観的ロックは通常、データベース ロックの実装に基づいて実装されます。スレッドが共有リソースにアクセスしたい場合、悲観的ロックによって定義されたメカニズムを使用してリソースのロックを取得します。このプロセス中に、他のスレッドもロックを取得したい場合は、現在のスレッドがロックを解放するまで待ってからロックを取得する必要があります。もちろん、タイムアウトやその他のメカニズムを設定することで、デッドロックなどの問題を回避することもできます。
Java では、synchronized キーワードを使用して悲観的ロックを実装できます。Synchronized はオブジェクト レベルまたはクラス レベルで使用できます。スレッドが同期ロックされたオブジェクトにアクセスすると、オブジェクトの状態はロック状態に設定されます。他のスレッドもこの状態を変更したい場合は、現在のスレッドがロックを解放するまで待ってからロックを取得する必要があります。この方法は理想的であるように見えますが、実際には同時実行数が少ないビジネス シナリオにのみ適用されます。
public class LockExample {
private int count = 0;
public synchronized void increment() {
// some code here
this.count++;
}
}
上記のコードでは、悲観的ロックを実装するために synchronized キーワードが使用されています。increment() メソッドでは、 this キーワードを使用してメソッド全体をロックし、このメソッドへのスレッドの実行中に他のスレッドが同時にこのメソッドにアクセスしないようにします。
悲観的ロックの欠点は、各スレッドがリソースにアクセスする前にロックを取得する必要があるため、同時実行性が非常に高い場合、多数のスレッドがロックを待機することになり、アプリケーション全体のパフォーマンスが低下することです。急激に下がります。
同時実行性の高いビジネス シナリオでは、デッドロックなどの問題が発生しやすく、アプリケーションのパフォーマンスが低下する可能性があるため、悲観的ロックを使用することは不適切です。
2. 楽観的ロックとは何ですか
悲観的ロックが防御的ロックである場合、楽観的ロックはリソースのロックを最大限に回避しようとする攻撃的ロックです。オプティミスティック ロックは、より「楽観的な」アイデアを具体化したものです。複数のスレッドが同じリソースにアクセスする可能性はそれほど高くないと考えられているため、共有リソースにアクセスするときに、リソースに対して特別なロック操作は実行されません。代わりに、変更操作はリソースに対して直接実行され、バージョン検証は変更操作の完了後に実行されます。読み取られたバージョン番号が現在のバージョン番号と一致する場合、操作は成功し、プログラムは実行を継続します。そうでない場合は、他のスレッドがリソースを変更したことを意味します。再試行操作を実行して、バージョン番号を読み取る必要があります。再度更新操作を行ってください。
Java では、オプティミスティック ロックを実装する主な方法が 2 つあります。1 つは CAS、つまり Compare And Swap で、もう 1 つはバージョン番号メカニズム (AtomicInteger クラスなど) です。
比較交換アルゴリズムを使用する
CAS は、CPU の基礎となるメモリ操作命令を許可することでアトミック操作を実装するメカニズムです。CAS メカニズムの動作原理は、現在のメモリの値と CAS 命令の値を比較することです。一致していれば、現在のメモリの値は新しい値に更新されます。一致していなければ、操作が再実行されます。
Compare-And-Swap (CAS) アルゴリズムは、非常に高い同時実行性を実現できるオプティミスティック ロックに基づくアルゴリズムです。基本的な考え方は、共有リソースを変更するときは、まずリソースの現在の状態を読み取り、次にその状態を比較するというものです。状態が変化していない場合は、リソースの状態を更新します。そうでない場合は、変更操作を無視し、次回の変更の機会を待ちます。
以下は、CAS アルゴリズムを使用して楽観的ロックを実装する例です。
public class CASExample {
private volatile int value;
public void increment() {
while (true) {
// 使用CAS算法进行操作
int current = this.value;
int next = current + 1;
if (compareAndSwap(current, next)) {
break;
}
}
}
// 比较并替换方法
private synchronized boolean compareAndSwap(int current, int next) {
if (this.value == current) {
this.value = next;
return true;
}
return false;
}
}
この例では、increment() メソッドは継続的にループして共有リソースの値を取得し、リソースを更新する必要があるかどうかを判断します。リソースを更新する必要がある場合、compareAndSwap() メソッドを呼び出して比較および置換操作を実行します。現在の値が読み取った値と同じである場合、古い値は新しい値に置き換えられます。
比較交換アルゴリズムでは、CAS アルゴリズムが実際の役割を果たすことができるのは、共有リソースが十分にホットで競合が激しい場合のみであることに注意してください。そうでない場合、CAS アルゴリズムは、ステータスを取得するために追加の操作を必要とするためです。現在のリソースにアクセスするため、そのパフォーマンスは従来の悲観的ロック メカニズムよりも低い可能性があります。
バージョン番号メカニズムを使用する AtomicInteger クラス
タイムスタンプ メカニズムとも呼ばれるバージョン番号メカニズムは、Redis などのキャッシュ操作を実行する場合によく使用されます。基本的な考え方は、制御する必要がある各リソース オブジェクトにバージョン番号フィールドを追加し、変更操作が実行されるたびにバージョン番号を更新する必要があるということです。変更が成功した場合、バージョン番号は +1 になります。そうでない場合、操作は実行されません。このように、リソースを読み取る操作を実行するときは、バージョン番号を比較するだけで済みます。バージョン番号が一致していれば、以降の操作を実行できることを意味し、そうでない場合は、他のスレッドがリソースを変更したことを意味し、再試行操作が必要です。
AtomicInteger クラスはスレッドセーフであり、楽観的ロックを簡単に実装できます。次のコードでは、AtomicInteger を使用してカウンターを実装し、incrementAndGet() メソッドを使用して自己インクリメント操作を実装します。
public class AtomicIntegerExample {
private AtomicInteger count = new AtomicInteger();
public void increment() {
// some code here
this.count.incrementAndGet();
}
}
3. 悲観的ロックと楽観的ロックの比較
実際の開発においては、悲観的ロックと楽観的ロックにはそれぞれメリットとデメリットがあり、開発者は具体的なビジネスシナリオに応じて柔軟に選択する必要があります。以下では、悲観的ロックと楽観的ロックを詳細に比較します。
3.1 実装の難易度
悲観的ロックの実装は比較的単純で、synchronized キーワードなどのメカニズムをコードに導入するだけです。楽観的ロックの実装はさらに困難です。CAS メカニズムを使用するときは、いくつかの下位レベルのテクノロジを使用する必要があり、実装プロセス全体が比較的煩雑で、特に同時実行性の高いシナリオでは境界条件を慎重に処理する必要があります。
3.2 パフォーマンス
同時実行性が高いシナリオでは、多くの場合、楽観的ロックのパフォーマンスは悲観的ロックのパフォーマンスよりも大幅に優れています。これは、悲観的ロックが共有リソースに対して頻繁にロックとロック解除の操作を実行するため、スレッドのブロックが容易に発生し、システムのパフォーマンスが低下する可能性があるためです。したがって、同時実行性が高いシナリオでは、オプティミスティック ロックを使用すると、プログラムの同時実行パフォーマンスを向上させることができます。
3.3 実装の複雑さ
悲観的ロックの実装は、本質的にはロック/ロック解除プロセスです。楽観的ロックには、バージョン管理やその他の側面が関与する必要があります。したがって、実装の複雑さの点では、悲観的ロックは楽観的ロックよりも大幅に低くなります。
3.4 データ競合の処理
悲観的ロックは共有リソースを常にロックし、他のスレッドからの干渉を排除するため、データ競合を効果的に回避できるため、データ競合を非常にうまく処理できます。楽観的ロックではバージョン番号を制御する必要があります。バージョン番号が一致しない場合、リソースを再試行する必要があります。再試行回数が多すぎると、再試行タイムアウトなどの問題が発生し、システムの通常の動作に影響を与える可能性があります。
4. まとめ
上記の比較から、悲観的ロックと楽観的ロックにはそれぞれメリットとデメリットがあることが分かりますが、実際の開発においては、ビジネス要件やアクセス頻度などに応じて柔軟に選択する必要があります。悲観的ロックは、同時実行パフォーマンスが低い場合にデータの整合性を保証できますが、リソースにアクセスするときに各スレッドがロックを取得する必要があるため、パフォーマンスがあまり良くない可能性があります。対照的に、オプティミスティック ロックでは、CAS アルゴリズムなどのテクノロジを使用してリソースの一貫性を確保し、それによってより高い同時実行パフォーマンスを実現できます。同時実行性が高いシナリオでは、オプティミスティック ロックによりプログラムの同時実行パフォーマンスを向上させることができますが、過剰な再試行を避けるためにバージョン番号の制御に注意を払う必要があります。ロック メカニズムに関係なく、データの一貫性とセキュリティを確保し、ダーティ リード、ファントム リード、アウトオブオーダー書き込みなどの安全でない状況を回避するように注意する必要があります。
追加のコード分析
完全なコードは次のとおりです。
import java.util.concurrent.atomic.AtomicInteger;
public class LockExample {
private int count1 = 0;
private AtomicInteger count2 = new AtomicInteger();
private volatile int count3;
public synchronized void increment1() {
this.count1++;
}
public void increment2() {
this.count2.incrementAndGet();
}
public void increment3() {
while (true) {
int current = this.count3;
int next = current + 1;
if (compareAndSwap(current, next)) {
break;
}
}
}
private synchronized boolean compareAndSwap(int current, int next) {
if (this.count3 == current) {
this.count3 = next;
return true;
}
return false;
}
}
このコードでは、CAS アルゴリズムの悲観的ロック、AtomicInteger 楽観的ロック、および楽観的ロックを示すために使用される 3 つの属性 count1、count2、および count3 を含む LockExample クラスを定義します。これらのプロパティはすべて、プロパティに対して自己インクリメント操作を実行するために使用される increment() メソッドを実装します。ここでは、synchronized キーワードを使用した悲観的ロック、AtomicInteger クラスを使用した楽観的ロック、CAS アルゴリズムを使用した楽観的ロックの 3 つの異なるロック メカニズムを使用します。上記のコードを通じて、Java マルチスレッドのロック メカニズムをよりよく理解できます。