1. ミューテックス(Mutex):何であり、どう動くか
ミューテックス(英語の「mutual exclusion」=「相互排他」)は、同時に1つのスレッドだけがクリティカルセクションのコードを実行できるようにする仕組みです。ミューテックスが占有中(他のスレッドが保持中)の場合、残りのスレッドは解放されるまで待機します。
Java では、コードが同期する対象オブジェクトに対して synchronized を使うことが多く、そのオブジェクトがミューテックスの役割を果たします。Java 5 以降には、より明示的で柔軟なミューテックス実装である ReentrantLock が導入されています。
イメージ
鍵が一つだけある部屋(ミューテックス)を想像してください。入室するには鍵を取る必要があります。鍵がなければ(誰かが既に取っていれば)、ドアの前で待ちます。鍵が元に戻った(ミューテックスが解放された)ら、次の人が入室できます。
Java におけるミューテックスの構文
synchronized を使う(古典的な方法):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
ここではメソッド increment 全体がミューテックスで保護されています。同時に実行できるのは常に1つのスレッドだけです。
ReentrantLock を使う(より柔軟):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // ミューテックスを取得
try {
count++;
} finally {
lock.unlock(); // 必ず解放する!
}
}
}
重要! 常に finally ブロックでミューテックスを解放してください。さもないと「永久ロック」(デッドロック)を引き起こし、プログラムがハングします。
ミューテックスが必要なときは?
リソースに同時にアクセスできるスレッドを1つに制限すべき場合にミューテックスが必要です。対象は変数、ファイル、データベースなどです。特に操作がアトミックでない場合には重要です。たとえば単純な count++ であっても、実際には値の読み取り・増加・書き戻しという3段階から成ります。ミューテックスがなければ、複数スレッドがこれらの間に割り込んでデータ競合を引き起こします。
2. セマフォ(Semaphore):何のために必要で、どう動くか
セマフォは、同時に一定数までのスレッドにリソースの利用を許可する「調整器」です。上限に達したら、他のスレッドは順番が来るまで待機します。
たとえば、3 台分の駐車場。すべての区画が埋まっている間は、新しく来た車は誰かが出るまで待ちます。
Java におけるセマフォの構文
これには java.util.concurrent パッケージの Semaphore クラスを使用します。
import java.util.concurrent.Semaphore;
public class ParkingLot {
private final Semaphore spots;
public ParkingLot(int places) {
this.spots = new Semaphore(places);
}
public void parkCar(String car) throws InterruptedException {
spots.acquire(); // 空きがあれば取得(なければ待機)
try {
System.out.println(car + " が駐車しました。");
Thread.sleep(1000); // 駐車中を模擬
} finally {
spots.release(); // 空きを解放
System.out.println(car + " が出発しました。");
}
}
}
使用例:
ParkingLot parking = new ParkingLot(3);
for (int i = 1; i <= 5; i++) {
final String car = "車 " + i;
new Thread(() -> {
try {
parking.parkCar(car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
結果:同時に駐車できる車は3台までで、その他は待機します。
セマフォはどう動く?
- セマフォ作成時に「許可数」(permit)を設定します。
- acquire() は許可の取得を試みます。空きがあれば通過し、なければ待機します.
- release() は許可を返却します。
- 許可数が1のセマフォは、ほぼミューテックスのように振る舞いますが「所有者」の概念がありません。
3. ミューテックスとセマフォ:何が違う?
| 特性 | ミューテックス(Mutex) | セマフォ(Semaphore) |
|---|---|---|
| 同時スレッド数 | 1つのみ | 複数(上限あり) |
| 用途 | リソースの保護 | アクセス制限(例:プール) |
| Java の API | |
|
| 管理 | 通常は「所有者」 | どのスレッドでも解放可能 |
| 典型的なシナリオ | 共有カウンタ、オブジェクト | 接続プール、駐車場、上限 |
- ミューテックス — 排他的アクセスが必要な場合に使う。
- セマフォ — 複数は通すが全員は通さない場合に使う。
たとえ話:ミューテックス=個室が1つのトイレ;セマフォ=個室が3つあるトイレ。
4. 実践的な課題例
例 1:クリティカルセクション保護のためのミューテックス
共通の銀行口座があり、複数スレッドが口座間で送金する状況を考えます。操作はアトミックでなければなりません。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccount(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
public void withdraw(int amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock();
}
}
public int getBalance() {
return balance;
}
}
ここでは、残高に対するあらゆる操作をミューテックスで保護し、競合状態(race condition)が発生しないようにしています。
例 2:アクセス制限のためのセマフォ
サーバーは同時に 2 クライアントしか処理できない(たとえばライセンスの都合で)とします。
import java.util.concurrent.Semaphore;
public class Server {
private final Semaphore connections = new Semaphore(2);
public void handleRequest(String client) throws InterruptedException {
connections.acquire();
try {
System.out.println(client + " がサーバーに接続しました。");
Thread.sleep(2000); // リクエスト処理の模擬
} finally {
connections.release();
System.out.println(client + " が切断しました。");
}
}
}
使用例:
Server server = new Server();
for (int i = 1; i <= 5; i++) {
final String client = "クライアント " + i;
new Thread(() -> {
try {
server.handleRequest(client);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
結果:同時にサーバーが対応するクライアントは2つまでです。
5. 使い方の特徴と注意点
ミューテックス:必ず解放する!
例外が発生する場合でも、unlock()(または同期ブロックからの退出)を忘れないことが非常に重要です。常に try-finally を使いましょう。
lock.lock();
try {
// クリティカルセクション
} finally {
lock.unlock();
}
解放を忘れると「永久ロック」が発生し、他のスレッドは永遠に待つことになります。
セマフォ:別スレッドが解放してよい?
ミューテックスと異なり、release() は acquire() を行っていないスレッドからも呼び出せます。便利な場合もありますが、誤用しやすいので運用ルールを守りましょう。
許可数が1の Semaphore はミューテックス?
ほぼ同様ですが、セマフォには「所有者」の概念がありません。誰が解放してもカウンタが増えます。一方ミューテックスは取得したスレッドが解放するのが前提です。
セマフォとプールを混同しない
セマフォはオブジェクトプールそのものではなく、あくまで「許可数のカウンタ」です。しばしば(たとえば DB 接続プールの)実装に使われますが、セマフォ自体は何も保持しません。
6. ミューテックスやセマフォでよくあるミス
エラー1:unlock/release を呼び忘れる。 ミューテックスやセマフォを取得したのに unlock() や release() を呼ばないと、他のスレッドが永遠にブロックされる可能性があります。例外時でも確実に解放されるよう、常に try-finally を使いましょう。
エラー2:誤ったオブジェクトで同期する。 すべてのスレッドで共有されていない変数(例:ローカル変数や文字列リテラル)に対して同期しても、同期は期待どおりに機能しません。
エラー3:二重に解放する。 セマフォの場合、acquire() の回数より多く release() を呼ぶと、許可数が上限を越えて増加します。バランスに注意してください。
エラー4:ミューテックスが必要なのにセマフォを使う(その逆も)。 排他的アクセスが必要ならミューテックス(synchronized や Lock)。同時実行数を制限したいなら Semaphore を使いましょう。
エラー5:ロックを長時間保持する。 スレッドがミューテックスやセマフォを長く保持するほど、他のスレッドの待ち時間が延びます。クリティカルセクション内の処理時間は最小化しましょう。
GO TO FULL VERSION