Javaのモニタパターン:モニタによる堅牢なロック機構の実装
別名
- 同期ブロック
モニタデザインパターンの目的
Javaのモニタデザインパターンは、並行操作を同期させ、スレッドの安全性を確保し、競合状態を防ぐために不可欠です。
現実世界の例を用いたモニタパターンの詳細な説明
現実世界の例
複数の従業員が使用する共有オフィスプリンタを想像してみてください。プリンタは、異なるドキュメントのページが混在しないように、一度に1つの印刷ジョブしか処理できません。このシナリオは、プログラミングにおけるモニタデザインパターンに似ています。
この例では、プリンタは共有リソースを表し、従業員はスレッドに類似しています。従業員は印刷ジョブを開始する前にプリンタへのアクセスを要求する必要があるシステムが設定されています。このシステムにより、一度に1人の従業員(または「スレッド」)だけがプリンタを使用できるようになり、ジョブ間の重複や干渉を防ぎます。印刷ジョブが完了すると、キュー内の次の従業員がプリンタにアクセスできます。このメカニズムは、複数の「スレッド」(従業員)による秩序立った安全な使用を保証するために、共有リソースへのアクセスを制御するモニタパターンの方法を反映しています。
簡単な言葉で
モニタパターンは、データへのシングルスレッドアクセスを強制するために使用されます。一度に1つのスレッドだけがモニタオブジェクト内のコードを実行できます。
Wikipediaによると
並行プログラミング(並列プログラミングとも呼ばれる)では、モニタは、スレッドが相互排除と特定の条件が偽になるのを待つ(ブロックする)機能の両方を持つことができる同期構成要素です。モニタには、他のスレッドに条件が満たされたことを知らせるメカニズムもあります。
Javaにおけるモニタパターンのプログラム例
モニタデザインパターンは、並行プログラミングで使用される同期手法であり、一度に1つのスレッドだけが特定のコードセクションを実行できるようにします。これは、オブジェクトのメソッド内に同期プリミティブ(セマフォやロックなど)をラップして隠す方法です。このパターンは、競合状態が発生する可能性のある状況で役立ちます。
Javaのモニタデザインパターンは、`Bank`クラスの例で見ることができます。同期メソッドを使用することにより、`Bank`クラスは、一度に1つのスレッドだけがトランザクションを実行できるようにし、現実世界のアプリケーションにおけるモニタパターンの効果的な使用を示しています。
追加のコメントが付いた`Bank`クラスの簡略版を以下に示します
public class Bank {
@Getter
private final int[] accounts;
public Bank(int accountNum, int baseAmount) {
accounts = new int[accountNum];
Arrays.fill(accounts, baseAmount);
}
public synchronized void transfer(int accountA, int accountB, int amount) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
if (accounts[accountA] >= amount && accountA != accountB) {
accounts[accountB] += amount;
accounts[accountA] -= amount;
}
}
public synchronized int getBalance() {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
int balance = 0;
for (int account : accounts) {
balance += account;
}
return balance;
}
public synchronized int getBalance(int accountNumber) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
return accounts[accountNumber];
}
}
`Main`クラスでは、銀行口座でトランザクションを実行するために複数のスレッドが作成されます。モニタとして機能する`Bank`クラスは、これらのトランザクションがスレッドセーフな方法で実行されることを保証します。
public class Main {
private static final int NUMBER_OF_THREADS = 5;
private static final int BASE_AMOUNT = 1000;
private static final int ACCOUNT_NUM = 4;
public static void runner(Bank bank, CountDownLatch latch) {
try {
SecureRandom random = new SecureRandom();
Thread.sleep(random.nextInt(1000));
for (int i = 0; i < 1000000; i++) {
bank.transfer(random.nextInt(4), random.nextInt(4), random.nextInt(0, BASE_AMOUNT));
}
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
var bank = new Bank(ACCOUNT_NUM, BASE_AMOUNT);
var latch = new CountDownLatch(NUMBER_OF_THREADS);
var executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
executorService.execute(() -> runner(bank, latch));
}
latch.await();
}
}
この例では、`Bank`クラスがモニタであり、`transfer`メソッドは相互排除の方法で実行する必要があるクリティカルセクションです。 Javaの`synchronized`キーワードは、モニタパターンを実装するために使用され、一度に1つのスレッドだけが`transfer`メソッドを実行できるようにします。
Javaでモニタパターンを使用する場合
モニタデザインパターンは、複数のスレッドまたはプロセスによって同時にアクセスおよび操作する必要がある共有リソースがある場合に使用してください。このパターンは、競合状態、データの破損、および不整合状態を防ぐために同期が必要なシナリオで特に役立ちます。モニタパターンの使用を検討すべき状況を以下に示します
**共有データ**: アプリケーションに、複数のスレッドによってアクセスおよび更新される必要がある共有データ構造、変数、またはリソースが含まれる場合。モニタは、一度に1つのスレッドだけが共有リソースにアクセスできるようにすることで、競合を防ぎ、データの整合性を確保します。
**クリティカルセクション**: 一度に1つのスレッドのみが実行する必要があるコードのクリティカルセクションがある場合。クリティカルセクションは、共有リソースが操作されるコードの部分であり、同時アクセスによって問題が発生する可能性があります。モニタは、一度に1つのスレッドだけがクリティカルセクションを実行できるようにします。
**スレッドセーフティ**: ロックやセマフォなどの低レベルの同期メカニズムのみに依存せずにスレッドセーフティを確保する必要がある場合。モニタは、同期とリソース管理をカプセル化する高レベルの抽象化を提供します。
**待機とシグナリング**: スレッドが続行する前に特定の条件が満たされるのを待つ必要があるシナリオがある場合。モニタには、スレッドが特定の条件を待機し、他のスレッドが条件が満たされたときに通知するためのメカニズムが含まれていることがよくあります。
**デッドロックの防止**: 共有リソースのロックを取得および解放するための構造化された方法を提供することにより、デッドロックを防ぎたい場合。モニタは、リソースアクセスが適切に管理されるようにすることで、一般的なデッドロックシナリオを回避するのに役立ちます。
**並行データ構造**: 複数のスレッドが整合性を維持しながら構造を操作する必要があるキュー、スタック、ハッシュテーブルなどの並行データ構造を実装する場合。
**リソースの共有**: 複数のスレッドが、データベースへの接続やネットワークソケットへのアクセスなど、限られたリソースを共有する必要がある場合。モニタは、これらのリソースの割り当てと解放を制御された方法で管理するのに役立ちます。
**保守性の向上**: 同期ロジックと共有リソース管理を単一のオブジェクト内にカプセル化して、コードの構成を改善し、並行性関連のコードについて推論しやすくする場合。
ただし、モニタパターンがすべての並行シナリオに最適とは限らないことに注意することが重要です。場合によっては、ロック、セマフォ、または並行データ構造などの他の同期メカニズムの方が適している場合があります。さらに、最新のプログラミング言語とフレームワークは、低レベルの同期の複雑さを抽象化する高レベルの並行性構成要素を提供することがよくあります。
モニタパターンを適用する前に、アプリケーションの並行性要件を徹底的に分析し、パフォーマンス、複雑さ、使用可能な言語機能などの要素を考慮して、ニーズに最適な同期アプローチを選択することをお勧めします。
Javaにおけるモニタパターンの実際のアプリケーション
Javaにおけるモニタデザインパターンの一般的な実装には、同期メソッドとブロック、および`Vector`や`Hashtable`などの並行データ構造が含まれます。
モニタパターンの利点とトレードオフ
利点
- 相互排除を保証し、競合状態を防ぎます。
- リソースアクセスのための明確な構造を提供することにより、スレッド管理の複雑さを軽減します。
トレードオフ
- ロックのオーバーヘッドにより、パフォーマンスが低下する可能性があります。
- 慎重に設計しないと、デッドロックが発生する可能性があります。
関連するJavaデザインパターン
セマフォ:複数のスレッドによる共通リソースへのアクセスを制御するために使用されます。モニタは、そのコアでバイナリセマフォの概念を使用します。
ミューテックス:相互排除を保証するための別のメカニズム。モニタは、ミューテックスを使用して実装されることが多い高レベルの構成要素です。