CodeGym /コース /JAVA 25 SELF /ミューテックスとセマフォ:構文と課題

ミューテックスとセマフォ:構文と課題

JAVA 25 SELF
レベル 52 , レッスン 3
使用可能

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
synchronized, Lock
Semaphore
管理 通常は「所有者」 どのスレッドでも解放可能
典型的なシナリオ 共有カウンタ、オブジェクト 接続プール、駐車場、上限

- ミューテックス — 排他的アクセスが必要な場合に使う。
- セマフォ — 複数は通すが全員は通さない場合に使う。

たとえ話:ミューテックス=個室が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:ミューテックスが必要なのにセマフォを使う(その逆も)。 排他的アクセスが必要ならミューテックス(synchronizedLock)。同時実行数を制限したいなら Semaphore を使いましょう。

エラー5:ロックを長時間保持する。 スレッドがミューテックスやセマフォを長く保持するほど、他のスレッドの待ち時間が延びます。クリティカルセクション内の処理時間は最小化しましょう。

1
タスク
JAVA 25 SELF, レベル 52, レッスン 3
ロック未解除
クリティカルイベントの監視 🚨
クリティカルイベントの監視 🚨
1
タスク
JAVA 25 SELF, レベル 52, レッスン 3
ロック未解除
滑走路アクセス管理 ✈️
滑走路アクセス管理 ✈️
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION