Java waitとは?使い方・sleepとの違い・notifyの仕組みを初心者向けに徹底解説

目次

1. java waitとは何か(結論を最短で理解する)

java wait は、あるスレッド(処理の流れ)を一時停止し、他のスレッドからの通知を待つためのメソッドです。
マルチスレッド処理(複数の処理を同時に動かす仕組み)で、スレッド間の協調制御を行うために使用します。

重要なのは、waitは「単なる時間待ち」ではないという点です。
他スレッドから notify() または notifyAll() が呼ばれるまで、対象スレッドは待機状態(WAITING)になります。

1.1 waitの基本的な意味

waitの本質は次の3点です。

  • 現在のスレッドを待機状態にする
  • 対象オブジェクトのロックを一時的に解放する
  • 他スレッドからの通知で再開する

概念図(流れ):

  1. synchronizedブロックに入る
  2. wait() を呼ぶ
  3. ロックを解放して待機状態へ
  4. 他スレッドが notify() を呼ぶ
  5. ロックを再取得後に処理再開

簡単な構文例:

synchronized(obj) {
    obj.wait();
}

⚠ 注意

  • waitは「一定時間止まる」のではなく、「通知を待つ」仕組みです。
  • 何も通知されなければ永遠に待ち続けます。

1.2 waitはObjectクラスのメソッド

waitは Thread クラスのメソッドではありません。
Objectクラスのメソッドです。

定義(概念):

public final void wait()

つまり、

  • すべてのJavaオブジェクトはwaitを持っている
  • 監視対象となるオブジェクトを基準に待機する

よくある誤解:

Thread.wait() と思っている
❌ スレッドを直接止めるメソッドだと思っている

正しくは:

✔ 「オブジェクトのモニター(監視ロック)上で待機する」

※モニター=synchronizedで管理されるロックのこと。

1.3 waitを使うための絶対条件

waitを使うには、必ず synchronized ブロック内で呼び出す必要があります。

理由:

  • waitは「現在保持しているロック」を解放する動作をするため
  • ロックを保持していないと整合性が取れない

誤った例:

Object obj = new Object();
obj.wait();   // ❌ IllegalMonitorStateException

正しい例:

Object obj = new Object();

synchronized(obj) {
    obj.wait();
}

発生する例外:

IllegalMonitorStateException

これは「ロックを持っていないのにwaitを呼んだ」ことを意味します。

⚠ 初心者が特につまずくポイント

  • sleepと混同する(sleepはロックを解放しない)
  • synchronizedを書き忘れる
  • notifyを呼ばないため永遠に停止する
  • 別のオブジェクトに対してnotifyしてしまう

✔ ここまでの最重要まとめ

  • waitはObjectクラスのメソッド
  • synchronized内でのみ使用可能
  • ロックを解放して通知を待つ仕組み
  • 単なる時間待ちではない

2. waitの正しい使い方(コード例で理解する)

waitは単体では意味を持ちません。
notify / notifyAll と組み合わせて初めて機能する仕組みです。

この章では、最小構成から実践パターンまで段階的に説明します。

2.1 基本構文

waitを正しく使う最小構成は次の通りです。

Object lock = new Object();

synchronized(lock) {
    lock.wait();
}

ただし、このままでは永遠に停止します。
理由:通知(notify)が存在しないため。

2.2 notifyとの組み合わせ例(最重要)

waitは「通知待ち」です。
したがって、別スレッド側で notify を呼び出す必要があります。

✔ 最小動作例

class Example {
    private static final Object lock = new Object();

    public static void main(String[] args) {

        Thread waitingThread = new Thread(() -> {
            synchronized(lock) {
                try {
                    System.out.println("Waiting...");
                    lock.wait();
                    System.out.println("Resumed.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread notifyingThread = new Thread(() -> {
            synchronized(lock) {
                System.out.println("Notifying...");
                lock.notify();
            }
        });

        waitingThread.start();
        notifyingThread.start();
    }
}

処理の流れ

  1. waitingThreadがlockを取得
  2. wait()でロックを解放し待機
  3. notifyingThreadがlockを取得
  4. notify()で待機スレッドに通知
  5. ロックを再取得後に処理再開

2.3 notifyとnotifyAllの違い

メソッド動作
notify()待機スレッドを1つだけ再開
notifyAll()すべての待機スレッドを再開

実務では notifyAll() の方が安全 とされる場合が多いです。

理由:

  • notify()では、再開すべきでないスレッドが選ばれる可能性がある
  • 条件が複数存在する場合に不整合が起きやすい

2.4 正しい実践パターン(whileで囲む)

waitは通常、条件判定とセットで使います。

synchronized(lock) {
    while (!condition) {
        lock.wait();
    }
}

理由:

  • スプリアスウェイクアップ(通知なし復帰)があり得る
  • notifyAll後に条件を満たしていない場合がある

❌ 間違い例:

if (!condition) {
    lock.wait();
}

これでは安全ではありません。

⚠ よくある失敗

  • waitとnotifyを別のオブジェクトに対して呼ぶ
  • notify後すぐ実行されると誤解する(ロック再取得が必要)
  • notifyをsynchronized外で呼ぶ
  • 条件変数を更新し忘れる

✔ 実装手順まとめ

  1. 共有ロックオブジェクトを作成
  2. synchronizedで囲む
  3. whileで条件判定
  4. waitで待機
  5. 別スレッドでnotify/notifyAll
  6. 条件を必ず更新する

3. waitとsleepの違い(検索意図の核心)

「java wait」で検索するユーザーの多くは、sleep() との違いを知りたいと考えています。
この2つは似ているように見えますが、設計思想も用途もまったく異なります。

3.1 決定的な違い(比較表)

項目waitsleep
所属クラスObjectThread
ロック解放するしない
使用場所synchronized内必須どこでも可
目的スレッド間通信単純な時間停止

3.2 最大の違いは「ロックを解放するか」

✔ waitの動作

  • 現在保持しているロックを解放する
  • 他スレッドがロックを取得できる
  • notifyで再開する
    synchronized(lock) {
        lock.wait();
    }
    

✔ sleepの動作

  • ロックを保持したまま停止
  • 他スレッドはロックを取得できない
    Thread.sleep(1000);
    

⚠ ここが最重要ポイント
sleepは「時間待ち」
waitは「通知待ち」

3.3 実務での使い分け

✔ waitを使うケース

  • Producer / Consumerパターン
  • データ到着待ち
  • 状態変更待ち
  • スレッド間の協調制御

✔ sleepを使うケース

  • 単純な遅延処理
  • リトライ間隔調整
  • テストコードでの待機

3.4 waitの時間指定

waitは時間指定も可能です。

lock.wait(1000);  // 最大1秒待機

ただし注意点:

  • 指定時間経過で復帰するが、条件が満たされている保証はない
  • whileでの再判定が必須

⚠ 初心者が混乱するポイント

  • sleepでも通知で復帰すると思っている → ❌
  • waitは時間で必ず再開すると誤解 → ❌
  • sleep中にロックが解放されると思っている → ❌

✔ 結論整理

  • sleepは単なる時間停止
  • waitはスレッド間通信の仕組み
  • ロック解放の有無が最大の違い

4. waitの応用知識(安全に使うための必須理解)

waitは単純に見えて、誤用すると不具合を生みやすいメソッドです。
ここでは、安全に実装するために必須の知識を解説します。

4.1 タイムアウト付きwait

waitには時間指定版があります。

synchronized(lock) {
    lock.wait(1000);  // 最大1秒待機
}

動作仕様:

  • 最大指定時間だけ待機
  • notifyがあれば即復帰
  • 時間経過でも復帰する

⚠ 重要
時間経過で復帰しても「条件が成立した」とは限りません。

必ず条件チェックを組み合わせます。

4.2 スプリアスウェイクアップとは

スプリアスウェイクアップ(Spurious Wakeup)とは、
通知なしでwaitが復帰する可能性がある現象です。

Java仕様上、これは起こり得るとされています。

つまり、

  • notifyされていない
  • 条件が変わっていない
  • それでも復帰する可能性がある

このため、ifではなくwhileが必須になります。

4.3 正しいパターン(whileで囲む)

正しい実装例:

synchronized(lock) {
    while (!condition) {
        lock.wait();
    }
    // 条件成立後の処理
}

理由:

  • スプリアスウェイクアップ対策
  • notifyAll後に条件未成立の可能性
  • 複数スレッド待機時の安全確保

❌ 危険な例:

synchronized(lock) {
    if (!condition) {
        lock.wait();  // 危険
    }
}

この書き方では、誤動作する可能性があります。

4.4 デッドロックとの関係

waitはロックを解放しますが、
複数ロックを扱う場合はデッドロックが発生します。

例:

  • スレッドA → lock1 → lock2
  • スレッドB → lock2 → lock1

対策:

  • ロック取得順序を統一
  • ロック数を最小限にする

⚠ よくある致命的ミス

  • 条件変数を更新しない
  • notifyだけして状態変更しない
  • synchronizedをネストしすぎる
  • 複数ロックで順序を揃えない

✔ 応用理解まとめ

  • waitはwhileで囲むのが絶対原則
  • タイムアウトでも条件再判定必須
  • ロック設計を誤るとデッドロック発生

5. よくあるエラーと解決方法

waitは低レベルAPIのため、実装ミスがそのまま不具合に直結します。
ここでは、実際によく発生するエラーと原因・対処法を整理します。

5.1 IllegalMonitorStateException

最も多いエラーです。

発生条件:

  • synchronized外でwaitを呼んだ
  • synchronized外でnotifyを呼んだ

❌ 誤り例

Object lock = new Object();
lock.wait();  // 例外発生

✔ 正しい例

synchronized(lock) {
    lock.wait();
}

対策まとめ:

  • wait / notifyは必ず同じオブジェクトのsynchronized内で呼ぶ
  • ロック対象オブジェクトを統一する

5.2 notifyしても再開しない

原因パターン:

  • 別オブジェクトに対してnotifyしている
  • 条件変数が更新されていない
  • notify後にロックを保持し続けている

❌ よくあるミス

synchronized(lock1) {
    lock1.wait();
}

synchronized(lock2) {
    lock2.notify();  // 別オブジェクト
}

✔ 必ず同一オブジェクトを使用する

synchronized(lock) {
    lock.wait();
}

synchronized(lock) {
    condition = true;
    lock.notifyAll();
}

5.3 waitが永遠に終わらない

主な原因:

  • notifyが一度も呼ばれていない
  • 条件判定ロジックの誤り
  • while条件が永遠にtrue

デバッグ手順:

  1. notifyが確実に呼ばれているか確認
  2. 条件変数が更新されているか確認
  3. ログ出力でスレッドの動作順を追う

5.4 デッドロック

典型例:

synchronized(lock1) {
    synchronized(lock2) {
        ...
    }
}

別スレッドで順序が逆だと停止します。

対策:

  • ロック取得順序を統一
  • 可能なら単一ロックにする
  • java.util.concurrentの利用を検討

5.5 InterruptedExceptionの扱い

waitは InterruptedException をスローします。

try {
    lock.wait();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

推奨対応:

  • 割り込みフラグを再設定する(interrupt再呼出)
  • 例外を握りつぶさない

⚠ 実務で特に多いミス

  • ifでwaitを囲む
  • notifyを条件更新前に呼ぶ
  • 複数ロック管理を曖昧にする
  • 例外を空catchにする

✔ エラー回避の基本原則

  • synchronized必須
  • 同一オブジェクト使用
  • whileパターン徹底
  • notify前に状態更新
  • interruptを適切に処理

6. 現代Javaでの代替手段(実務視点)

wait / notify はJava初期から存在する低レベルのスレッド制御手段です。
しかし現在の実務では、より安全で高水準なAPIを使うことが一般的です。

この章では、なぜ代替手段が推奨されるのかを整理します。

6.1 wait/notifyは低レベルAPI

waitを使う場合、開発者がすべてを手動管理します。

管理対象:

  • ロック制御
  • 条件変数
  • 通知順序
  • スプリアスウェイクアップ対策
  • デッドロック回避
  • 割り込み処理

つまり、バグの温床になりやすい設計です。

特に複数条件・複数スレッドが絡むと急激に複雑化します。

6.2 代表的な代替クラス

Java5以降は java.util.concurrent パッケージが提供されています。

✔ BlockingQueue

スレッド間データ受け渡し用。

BlockingQueue<String> queue = new LinkedBlockingQueue<>();

queue.put("data");   // 自動で待機制御
queue.take();        // データが来るまで待つ

特徴:

  • wait/notify不要
  • デッドロックが起きにくい
  • 実務で非常に多用される

.

✔ CountDownLatch

複数スレッドの完了待ち。

CountDownLatch latch = new CountDownLatch(1);

latch.await();   // 待機
latch.countDown();  // 通知

用途:

  • 初期化完了待ち
  • 並列処理の同期

✔ ReentrantLock + Condition

waitに近い仕組みを、より明示的に扱えるAPI。

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    condition.await();
} finally {
    lock.unlock();
}

利点:

  • 複数条件を管理可能
  • ロック制御が柔軟

6.3 実務では何を使うべきか

状況別の推奨:

目的推奨手段
データ受け渡しBlockingQueue
単純同期CountDownLatch
複雑条件制御ReentrantLock + Condition
学習目的wait理解必須

結論:

  • 学習段階ではwaitを理解することは重要
  • 実務ではconcurrentパッケージを優先

⚠ 現場での判断基準

  • スレッド制御を自作する必要があるか?
  • 標準ライブラリで代替できないか?
  • 可読性・保守性を確保できるか?

多くの場合、waitは使わなくても解決できます。

✔ 本章まとめ

  • waitは低レベルAPI
  • concurrentパッケージが現代標準
  • 実務では安全性を優先する

7. まとめ(初心者向け最終整理)

java wait は、スレッド間で状態変化を待つための仕組みです。
単なる時間停止ではなく、「通知を待つ制御メカニズム」である点が最重要です。

ここまでの内容を、実装視点で整理します。

7.1 waitの本質

  • Objectクラスのメソッド
  • synchronized内でのみ使用可能
  • 呼び出すとロックを解放して待機する
  • notify / notifyAllで再開する

7.2 安全に書くための3原則

① synchronized必須
② whileで囲む
③ notify前に状態更新

正しいテンプレート:

synchronized(lock) {
    while (!condition) {
        lock.wait();
    }
}

通知側:

synchronized(lock) {
    condition = true;
    lock.notifyAll();
}

7.3 sleepとの決定的な違い

項目waitsleep
ロック解放するしない
用途スレッド間通信時間待機

混同しないことが重要です。

7.4 実務での立ち位置

  • 理解しておくことは必須
  • しかし実務では concurrent パッケージを優先
  • 自前のwait制御は慎重に設計する

7.5 最終チェックリスト

  • 同一オブジェクトを使っているか
  • synchronized内で呼んでいるか
  • whileで囲んでいるか
  • notify前に状態更新しているか
  • interrupt処理をしているか

これらを守れば、重大な不具合は大Rememberに減らせます。

よくある質問

Q1. waitはThreadクラスのメソッドですか?

いいえ。Objectクラスのメソッドです。

Q2. synchronizedなしでwaitは使えますか?

使えません。IllegalMonitorStateExceptionが発生します。

Q3. notifyとnotifyAllはどちらを使うべきですか?

安全性重視ならnotifyAllを推奨します。

Q4. waitは時間指定できますか?

可能です。wait(long millis)を使用します。

Q5. ifではなくwhileを使う理由は?

スプリアスウェイクアップや条件未成立復帰を防ぐためです。

Q6. 実務でwaitはよく使われますか?

近年はjava.util.concurrentの利用が主流です。

Q7. sleepで代用できますか?

できません。sleepはロックを解放しません。