1. ロックの概要
ロック (Lock) は、共有リソースを保護し、複数のスレッドが同じリソースに同時にアクセスまたは変更することによって引き起こされるデータの不整合や競合状態を防ぐために使用される同時実行制御メカニズムです。ロックは相互排他アクセス メカニズムを提供し、特定の時点で 1 つのスレッドだけがロックを取得してクリティカル セクション コードを実行できるようにします。(ロックの本質は一種のリソースであり、オペレーティング システムによって維持される同期に特別に使用される一種のリソースです)
ロックは同時プログラミングにおいて重要な役割を果たし、スレッドの安全性とデータの一貫性を達成するために使用できます。複数のスレッドが共有リソースにアクセスする必要がある場合、ロックを取得することでリソースを使用する権利を競合します。スレッドがロックを取得した場合、他のスレッドは、ロックを保持しているスレッドがロックを解放するまで待つ必要があります。これにより、一度に 1 つのスレッドだけがクリティカル セクション コードにアクセスできるようになり、データの競合や不整合が回避されます。
一般的なロックには、組み込みロック (Java の synchronized キーワードなど)、明示的ロック (Java の ReentrantLock クラスなど)、読み取り/書き込みロック (Java の ReentrantReadWriteLock クラスなど) などが含まれます。相互排他的アクセスを提供することに加えて、ロックは再入性 (スレッドが同じロックを複数回取得できる)、割り込み可能性 (ロックの待機中にスレッドを中断できる)、および公平性 (スレッドが待機する順序でロックを取得できる機会) もサポートできます。
ロックを合理的に使用することにより、マルチスレッド プログラムの正確性と効率性が保証され、共有リソースへの同時アクセスによって引き起こされる問題を回避できます。ただし、ロックを過度に使用すると、デッドロックやパフォーマンスのボトルネックなどの問題が発生する可能性があるため、並列プログラムを設計する際には、状況に応じて適切なロック機構を選択する必要があります。
1.1 Java のロック
Java のロック機構は主に Monitor Lock に基づいて実装されています。モニターは、スレッドの相互排他アクセスを実装し、スレッド間の通信を調整するために使用される Java の同期メカニズムです。
Java では、各オブジェクトには組み込みモニター (ロックとも呼ばれます) があり、synchronized キーワードを通じて取得および解放できます。スレッドが同期されたコード ブロックまたはメソッドに入ると、対応するオブジェクトのモニターを取得しようとします。モニターがすでに他のスレッドによって占有されている場合、スレッドはモニターを取得できるまでブロック状態になり、実行を継続します。
モニター ロック メカニズムに基づいて、Java は相互排他的アクセスのサポートを提供し、同時に 1 つのスレッドだけがクリティカル セクション コードを実行できるようにすることで、データの競合や不整合の問題を回避します。同時に、Java のモニター ロックは、再入可能 (同じスレッドが同じロックを複数回取得できる)、内部ロック、待機/通知メカニズムなどの機能も提供します。
モニター ロックに基づく synchronized キーワードに加えて、Java は、明示的ロックの実装である ReentrantLock クラスなど、より柔軟なロック実装も提供します。組み込みのモニター ロックと比較して、明示的ロックは、時間指定可能なロックや割り込み可能なロックなど、より多くの機能を提供できます。
つまり、Java のロック機構は主にモニターロックに基づいて実装されており、synchronized キーワードまたは明示的ロックを使用することで、スレッドの同時アクセスと同期操作を効果的に管理できます。
1.2 関連記事:
同期&監視ロック_Doテストニャーソースのブログ - CSDNブログ
2. ロックの分類
ロックは次の寸法に従って分割できます。
1. 所有権のモード: ロックの所有権のモードは、ロックの取得と解放を指し、主に次の 2 つのタイプに分けられます。
- 排他的ロック: ロックを取得できるのは 1 つのスレッドだけであり、他のスレッドは待機する必要があります。一般的な排他ロックには、組み込みロック (同期) と明示的ロック (ReentrantLock など) が含まれます。
- 共有ロック: 複数のスレッドが同時にロックを取得し、共有リソースを同時に読み取ることができますが、他のスレッドによる書き込みは禁止されます。一般的な共有ロックは、読み取り/書き込みロック (ReentrantReadWriteLock) です。
2. 同時実行戦略: ロックの同時実行戦略は、同時環境におけるロックのリソース アクセス戦略を指します。一般的な同時実行戦略は次のとおりです。
- オプティミスティック ロック: 同時実行制御は、通常はバージョン番号 (バージョニング) またはタイムスタンプ (タイムスタンプ) を使用して、リソースの状態を識別することによって実行されます。
- 悲観的ロック: デフォルトでは、共有リソースを保護するために排他ロックが使用され、リソースにアクセスする前にロックが取得され、他のスレッドによるアクセスがブロックされます。
3. アプリケーション シナリオ: ロックのアプリケーション シナリオは、特定のニーズと使用条件に応じて選択できます。一般的なアプリケーション シナリオは次のとおりです。
- 組み込みロック (同期): オブジェクトを保護するために使用されるクリティカル セクション コードは、スレッド セーフを実現するために使用されます。
- 明示的ロック (ReentrantLock など): 再入可能、割り込み可能、公平性、その他の機能など、より柔軟なロック操作を提供します。
- 読み取り/書き込みロック (ReentrantReadWriteLock): 読み取りが多く書き込みが少ない状況に適しており、読み取り共有と書き込み排他の同時実行制御を提供します。
要約すると、ロックは、その所有方法、同時実行戦略、およびアプリケーション シナリオに従って分類できます。具体的な分類は、プログラミング言語やフレームワーク、使用環境によっても異なります。さまざまな種類のロックにはそれぞれ長所と短所があるため、適切なロック メカニズムを選択すると、同時実行パフォーマンスとデータの一貫性が向上します。
3.楽観的ロックと悲観的ロック
3.1 楽観的ロック
オプティミスティック ロックは、同時操作中のデータ一貫性の問題に対処するために使用される同時実行制御メカニズムです。操作全体を通じてロックを保持する必要がある悲観的ロック (悲観的ロック) と比較して、楽観的ロックは、操作中に競合が発生しないことを前提とした、より緩やかな戦略を採用します。
楽観的ロックの基本的な考え方:
データを更新するたびに、現在のバージョン番号(またはタイムスタンプなどのデータの状態を示す値)を読み取り、データのバージョン番号が変更されているかどうかを確認してから変更操作を実行します。データのバージョン番号が変更されていない場合は、操作中に他のスレッドがデータを変更していないことを意味し、オプティミスティック ロックは操作が成功すると信じますが、それ以外の場合は、他のスレッドがデータを変更したことを意味します。この時点で、操作に競合が発生する可能性があるため、それに応じて対処する必要があります。
オプティミスティック ロックの実装では、通常、データのバージョンを識別するためにバージョン番号フィールドまたはタイムスタンプ フィールドが使用されます。スレッドがデータを読み取るとき、同時に現在のバージョン番号を記録します。スレッドがデータを更新する場合、更新操作を実行する前に、現在のバージョン番号が最初に記録されたバージョン番号と一致するかどうかを再度確認します。一貫性がある場合は、データが他のスレッドによって変更されていないことを意味し、更新操作を実行してバージョン番号を増やすことができます。矛盾している場合は、データが他のスレッドによって変更されていることを意味します。この時点で、操作を中止するか、操作を再試行するか、他のロジックで競合を処理するかを選択できます。
楽観的ロックの利点:
ただし、ほとんどの場合、ロックする必要はないため、スレッド間の競合や待機が回避されます。これにより、同時実行パフォーマンスとスループットが向上します。
質問:
オプティミスティック ロックにもいくつかの問題があります。たとえば、競合によって更新エラーが発生する可能性があり、適切な再試行メカニズムが必要です。複数の操作を必要とする複雑なシナリオの場合、オプティミスティック ロックの実装はより複雑になる可能性があります。
実際のアプリケーションでは、オプティミスティック ロックは通常、バージョン番号、タイムスタンプ、ハッシュ値などのメカニズムと組み合わせて使用され、シンプルで効率的な同時実行制御を提供します。データベースの分野では、同時更新の問題を解決するためにオプティミスティック ロックがよく使用され、分散システムにも同様のアプリケーションがあります。
3.1.1 データベースへの楽観的ロックの適用
データベースでは、同時更新シナリオを処理してデータの一貫性と整合性を確保するために、オプティミスティック ロックがよく使用されます。以下に、データベースにおける一般的なオプティミスティック ロックの適用方法をいくつか紹介します。
- バージョン管理: データ テーブルに新しいバージョン番号フィールドを追加すると、更新操作ごとにこのフィールドの値が更新されます。更新操作を実行する場合は、まず現在のデータのバージョン番号を読み取り、バージョン番号が一致しているかどうかを確認してから更新操作を実行します。一貫性がある場合は、他のスレッドがデータを変更していないことを意味し、更新操作を実行してバージョン番号を増やすことができます。一貫性がない場合は、他のスレッドがデータを変更したことを意味します。この時点で、操作を放棄するか、操作を再試行するか、競合を処理するかを選択できます。
- タイムスタンプ: バージョン番号と同様に、タイムスタンプはデータの変更時刻を記録するために使用されます。更新操作を実行する前に、現在のデータのタイムスタンプと最初の読み取り時に記録されたタイムスタンプを比較することによって、データが他のスレッドによって変更されたかどうかが判断されます。
- ハッシュ値: データの内容からハッシュ値を生成し、そのハッシュ値をデータ テーブルに保存します。更新操作が実行されると、データのハッシュが再計算され、最初に読み取られたときに保存されたハッシュと比較されます。ハッシュ値が同じであれば、データは変更されておらず更新可能であることを意味し、ハッシュ値が異なる場合は、他のスレッドによってデータが変更されたことを意味します。
- チェック列: データテーブルに追加のチェック列を追加して、データのステータス情報を記録します。更新操作を実行する前に、チェック欄の状態に応じて更新操作が実行できるかどうかが判断されます。
オプティミスティック ロックを適用するには、通常、SQL ステートメントで WHERE 句を使用して条件を判断したり、オプティミスティック ロックに関連する関数やステートメントを使用して同時実行性の競合に対処したりするなど、特定のプログラミング サポートが必要です。実際のアプリケーションでは、オプティミスティック ロックの具体的な実装は、データベースの種類、特性、ビジネス要件によって異なります。
3.1.2 Javaでの楽観的ロックの適用
Java では、オプティミスティック ロックの適用には通常、マルチスレッド環境での共有データへの同時更新が含まれます。以下に、楽観的ロックの一般的な実装とその例をいくつか示します。
1. バージョン番号を使用する (バージョン管理)
public class OptimisticLockExample {
private int value;
private int version;
public synchronized void updateValue(int newValue) {
// 保存原始版本号
int oldVersion = version;
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查版本号是否发生变化
if (oldVersion == version) {
value = newValue;
version++; // 修改版本号
System.out.println("Update succeeded!");
} else {
System.out.println("Update failed due to concurrent modification!");
}
}
}
2. Atomic クラスを使用します。
import java.util.concurrent.atomic.AtomicReference;
public class OptimisticLockExample {
private AtomicReference<Integer> value = new AtomicReference<>();
public void updateValue(int newValue) {
// 获取当前值和版本号
Integer oldValue = value.get();
Integer oldVersion = oldValue != null ? oldValue.hashCode() : null;
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查值和版本号是否发生变化
if (value.compareAndSet(oldValue, newValue)) {
System.out.println("Update succeeded!");
} else {
System.out.println("Update failed due to concurrent modification!");
}
}
}
ここでは AtomicReference クラスを使用してアトミック操作を保証し、hashCode() メソッドを使用してバージョン番号を生成します。CompareAndSet() メソッドは、古い値が現在の値と等しいかどうかを比較し、等しい場合は更新します。
オプティミスティック ロックの特定の実装では、ロックフリー データ構造 (CAS アルゴリズムなど)、データベースオプティミスティック ロック プラグイン、オプティミスティック ロック アノテーションなども使用できます。これらの方法はすべて、同時操作によって引き起こされるデータの競合を回避し、特定のメカニズムまたはアルゴリズムを通じてデータの一貫性を確保することを目的としています。実際のアプリケーションでは、シナリオに合ったオプティミスティック ロックの実装を選択することが非常に重要です。
3.1.2.1 拡張機能:
原子性:
アトミック性とは、操作が完全に正常に実行されるか、まったく実行されず、中間状態や部分的な実行が存在しないことを意味します。アトミック性により、マルチスレッド環境における操作の一貫性と信頼性が保証されます。
同時プログラミングでは、共有データの読み取りおよび書き込み操作においてアトミック性が非常に重要です。複数のスレッドが同時に同じデータにアクセスして変更する場合、アトミック性が保証されていないと、データの不整合、競合状態、その他の問題が発生する可能性があります。
アトミック性を確保するために、Java は次のようなさまざまなメカニズムとクラスを提供します。
- synchronized キーワード: synchronized キーワードを使用すると、メソッドまたはコード ブロックを同期済みとしてマークし、1 つのスレッドのみが同期領域に入ることができるようになり、同時アクセスが防止されます。
- Lock インターフェイスとその実装クラス: Lock インターフェイスは、再入可能性や公平性などの機能を提供する ReentrantLock などの synchronized よりも柔軟なロック メカニズムを提供します。
- アトミック クラス: java.util.concurrent.atomic パッケージの Atomic クラスは、特定の操作のアトミック性を保証する AtomicInteger、AtomicLong などのいくつかのアトミック操作クラスを提供します。
- アトミック コンテナ: Java の ConcurrentHashMap や ConcurrentLinkedQueue などのコンテナ クラスは、アトミックな操作を提供し、同時操作によって引き起こされるデータの不整合を回避します。
つまり、操作のアトミック性を確保することで、同時プログラミングにおけるデータ競合の問題を排除し、マルチスレッド環境でのデータの一貫性を確保できます。同期メカニズムとアトミック操作を適切に使用すると、プログラムの同時実行パフォーマンスと正確性を効果的に向上させることができます。
3.2 悲観的ロック
3.2.1 悲観的ロックとは
悲観的ロックは、共有リソースへの同時アクセス中に競合が発生する可能性が高いという悲観的な仮定に基づいた同時実行制御メカニズムです。したがって、悲観的ロックを使用する場合、プログラムは他のスレッドが共有リソースを変更することを想定し、競合やデータの不整合を防ぐために適切な措置を講じます。
悲観的ロックの主な特徴は、共有リソースにアクセスする前に、現在のスレッドがリソースを確実に独占できるようにロックを取得し、操作が完了した後にロックが解放されることです。悲観的ロックの実装には、ミューテックス、同期キーワード、データベース ロックなどの使用が含まれます。
スレッドが悲観的ロックを取得すると、他のスレッドはロックが解放されるまで待つ必要があります。これにより、同時に 1 つのスレッドだけが共有リソースを変更できるようになり、データ競合や同時実行性の競合の問題が回避されます。ただし、悲観的ロックは共有リソースに同時にアクセスできるスレッドの数を制限するため、同時実行性の高い環境ではパフォーマンスの低下につながる可能性があります。
悲観的ロックは、共有リソースが頻繁に変更されるシナリオに適しており、データのセキュリティと一貫性を確保するための保守的な同時実行制御方法を提供します。ただし、オプティミスティック ロックなどのより効率的な同時実行制御メカニズムの出現により、一部のシナリオではペシミスティック ロックが最適な選択ではなくなる可能性があります。
3.2.2 Java での悲観的ロックの適用
Java では、悲観的ロックの適用には、通常、共有データのクリティカル セクションの保護が含まれます。悲観的ロックでは、共有データにアクセスする前に、他のスレッドがデータを変更することを想定するため、最初にロックを取得し、次に操作を実行し、操作が完了した後にロックを解放します。一般的な悲観的ロックの実装とその例をいくつか示します。
synchronized キーワードを使用します。
public class PessimisticLockExample {
private int value;
public synchronized void updateValue(int newValue) {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = newValue;
System.out.println("Update succeeded!");
}
}
この例では、synchronized キーワードを使用してメソッドを変更することで、1 つのスレッドが updateValue() メソッドを実行するときに、他のスレッドが同時にメソッドに入ることができないことが保証され、それによって共有データへの相互排他的アクセスが保証されます。
Lock インターフェースとその実装クラスを使用します。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private int value;
private Lock lock = new ReentrantLock();
public void updateValue(int newValue) {
lock.lock(); // 获取锁
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = newValue;
System.out.println("Update succeeded!");
lock.unlock(); // 释放锁
}
}
この例では、ReentrantLock 実装クラスを使用してロック オブジェクトが作成され、ロックの取得と解放のために保護する必要があるコードのクリティカル セクションの前後で lock() メソッドとunlock() メソッドが呼び出されます。
悲観的ロックの特定の実装では、データベース ロック メカニズム、分散ロックなども使用できます。これらのメソッドはすべて、ロックすることで共有リソースの排他性を確保し、他のスレッドが同時にデータにアクセスしたり変更したりするのを防ぐように設計されています。ただし、悲観的ロックはスレッドの競合の増加と同時実行パフォーマンスの低下につながるため、実際のアプリケーションでは、悲観的ロックを使用するシナリオとコストを比較検討する必要があります。