読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目69:wait と notify よりコンカレンシーユティリティを選ぶ(後半)

固有のロック

Java は相互排他ロックを言語固有の機能として持っています。 このロックを利用するためには synchronzied ブロックを使います。

固有のロックは次のような仕組みで動作します。

  1. synchronized ブロックに入る時、スレッドは指定されたオブジェクトの固有のロックを自動的に取得します
    1. スレッドがロックを取得できたら、ブロック内の処理を実行します
    2. 他のスレッドがロックを取得している場合、ロックが取得できるまでスレッドの実行は待機します
  2. synchronized ブロックから出る時、スレッドは指定されたオブジェクトのロックを自動的に解放します
  3. 当該オブジェクトのロック取得待ちをしていたスレッドの1つがロックを取得し、後続の処理を実行します

例えば次のようなコードを2つのスレッドAとBで実行するとします。

synchronized (obj) { // obj は Object 型のインスタンス
    ...処理A...
    ...処理B...
}
  1. まず、スレッド A がこのコードブロックを実行します。その際、スレッド A が obj の固有のロックを自動的に取得します。
  2. この時、スレッド B がこのコードを実行しようとします。その際、スレッド B は obj の固有のロックを自動的に取得しようとします。ただし、スレッドAによってロックは占有されていますので、解放されるまで待機します。
  3. この間に スレッド A は処理Aと処理Bを実行します。synchronized ブロックを抜ける時、スレッド A は obj の固有のロックは解放します。
  4. すると、待機していたスレッドBが解放されたロックを取得します。最後に、スレッドBはコードを実行し、ブロックをぬけた時に obj の固有のロックは解放されます。

なお、同じスレッドは同じオブジェクトのロックを取得することができます。 これを再突入可能といいます。

wait() と notify()

wait() と notify() は Java のスレッド間の調整を行うためのメソッドです。 より正確には条件キュー(Condition Queue)と呼ばれる機構を操作するための API です。

Java のすべてのオブジェクトは条件キューを持っていて、Object クラスの wait() と notify() メソッドによってこのキューを操作することができます。

Object.wait() を呼び出すと、呼び出したスレッドはそのオブジェクトの条件キューに入ります。 そして、そのスレッドはそこでブロックします。

Object.notify()、または Object.notifyAll() を呼び出すと、そのオブジェクトの条件キューに入っているどれかのスレッドのブロックを解放します。 どのスレッドが解放されるかは保証されません。

wait()、notify()、notifyAll() を呼び出すためにはロックが必要です。

synchronized(obj) { // obj のロックを取得する必要がある
    obj.wait(); // スレッドは条件キューに入り、スレッドの動作は止まる。そして、wait() はロックを解放する
}

一方、notify() や notifyAll() は条件キューに入ったスレッドを解放し、wait() の待ちを解除します。

synchronized(obj) { // obj のロックを取得する必要がある
    // 条件キューに含まれているすべてのスレッドのブロックを解放する
    obj.notifyAll();
}

wait() と notify() のイディオム

前述のように wait() と notify() は Java の条件キューを操作する低レイヤな API です。 wait() と notify() を正しく使うために必要なイディオムを次の通りです。

  • Object.notify(), Object.wait() を呼ぶ時はロックを取得します。
  • while ループで条件が満たされてない場合はループさせます
    • while ループ外で wait() を呼び出してはいけません
  • なるべく notifyAll() を使うべきです。

なぜなら、次のようなケースが考えられるからです。

  • 条件が満たされた状態で wait() に入ってしまうと、notify() が永久によばれない可能性がある
    • このために do-while ではなく while を使う必要があります
  • 条件が満たされていないのに wait() が目覚める可能性がある
    • このために if ではなくループを使う必要があります
  • 他の場所で notifyAll() が使われると、すべての wait() が目覚める
  • 待ちスレッドは希に偽りの目覚めを起こす場合がある。
  • 他のスレッドが悪意をもって notify を呼び出す可能性がある。

このイディオムを擬似コードにすると次のようになります。

// wait を利用するための標準イディオム
synchronized (obj) {
        while (<条件が満たされていない>) {            
            obj.wait();
        }
        // 満たされた条件に対して適切な処理を行う
    }
}

一般的にスレッドを起こす際には notifyAll() を使うべきですが、注意深く使えば notify() の方がパフォーマンスが少し高いです。

このように wait() や notify() を使うためには複雑なイディオムが必要となるので、通常は java.util.concurrent の並行ライブラリをそのまま利用するべきです。