Javaにおけるwait/notify/notifyAllの使い方と同期制御

複数スレッドが協調して動作する際、あるスレッドが条件を満たすまで待機し、他のスレッドがその条件を満たした際に通知するというパターンは非常に一般的です。この仕組みを実現するための基本的なAPIがObject.wait()Object.notify()、およびObject.notifyAll()です。

同期待機の典型的な実装手法

以下のように、サブスレッドの処理完了を待つ必要があるケースを考えます:

    private volatile boolean isReady = false;

    public void executeWithWait() {
        final Object monitor = new Object();

        Thread worker = new Thread(() -> {
            synchronized (monitor) {
                try {
                    Thread.sleep(3000);
                    System.out.println("=== 処理完了(3秒経過)");
                } catch (InterruptedException ignored) {}
                isReady = true;
                monitor.notify(); // 待機中のスレッドを1つ起床
            }
        });

        worker.start();

        synchronized (monitor) {
            while (!isReady) {
                try {
                    System.out.println("=== 待機中...");
                    monitor.wait(); // ロックを解放して待機
                } catch (InterruptedException ignored) {}
            }
        }

        System.out.println("=== メインスレッド続行");
    }

この実装では、volatile変数による可視性保証とwait/notifyによる効率的な待機を組み合わせています。whileループで条件を再チェックするのは、擬似起床(spurious wakeup)に対応するための必須のパターンです。

内部メカニズム:ロックプールと待機プール

Javaの各オブジェクトは、同期制御のために2つの状態管理領域を持ちます:

  • ロックプール:synchronizedブロックへのアクセスを待っているスレッド群
  • 待機プール:wait()によって一時停止されたスレッド群

wait()を呼び出すと、現在のスレッドはロックを解放し、待機プールに移動します。notify()は待機プールからランダムに1スレッドを選び、ロックプールへ移動させます。notifyAll()は待機プールの全スレッドをロックプールへ移動させます。

notify vs notifyAll の選択基準

単一の待機条件で、起床すべきスレッドが1つだけでよい場合はnotify()で十分です。しかし、複数の異なる条件で待機している場合や、すべての待機スレッドに通知が必要な場合はnotifyAll()を使用すべきです。特に「生産者-消費者」パターンでは、notifyAll()が安全な選択肢となります。

生産者-消費者モデルの実装例

class Storage {
    private static final int CAPACITY = 10;
    private int itemCount = 0;
    private final Object lock = new Object();

    public void produce(int amount) throws InterruptedException {
        synchronized (lock) {
            while (itemCount + amount > CAPACITY) {
                System.out.println("在庫満杯: 待機開始");
                lock.wait();
            }
            Thread.sleep(100); // 生産処理のシミュレーション
            itemCount += amount;
            System.out.println("生産: +" + amount + ", 現在: " + itemCount);
            lock.notifyAll(); // 消費可能になったことを通知
        }
    }

    public void consume(int amount) throws InterruptedException {
        synchronized (lock) {
            while (itemCount < amount) {
                System.out.println("在庫不足: 待機開始");
                lock.wait();
            }
            Thread.sleep(100); // 消費処理のシミュレーション
            itemCount -= amount;
            System.out.println("消費: -" + amount + ", 現在: " + itemCount);
            lock.notifyAll(); // 生産可能になったことを通知
        }
    }
}

この実装では、ストレージの状態変化時に適切なタイミングでスレッドを起床させることで、リソースの無駄な競合を回避しています。

重要な注意点

  • wait/notifyは必ずsynchronizedブロック内で呼び出すこと
  • 待機条件は常にwhileループでチェックすること(擬似起床対策)
  • notify()は特定のスレッドを指定できないため、条件が複雑な場合はnotifyAll()が安全
  • 通知後にロックが即座に解放されるわけではない(synchronizedブロック終了時まで保持)

タグ: Java wait notify notifyAll マルチスレッド

7月5日 20:44 投稿