Javaにおけるスレッドの状態待ち
背景
マルチスレッドの処理において、排他制御の為にロックを取得し、期待する状態を待って後続処理を続行する。この様なよくある処理に関して、詳細を以下にまとめます。
実装例
早速ですが、スレッドの状態待ちにの実装例です。概要は以下の通りです。
ready
,count
: 各スレッドで共有する値です。- タスク1:
ready
ステータスがtrue
だった際に、count
に10加算する。 - タスク2:
ready
ステータスをtrue
に変更する。
public class ThreadSleepSample { // スレッド間で共有する値 private static boolean ready = false; // スレッド間で共有する値 private static int count = 0; // 各タスク内で呼び出しを行うインスタンスを生成する。 private static ThreadSleepSample threadSleepSample = new ThreadSleepSample(); // タスク1 private static Callable<Void> addCountTask = () -> { threadSleepSample.addCount(); return null; }; // タスク2 private static Callable<Void> changeReadyTask = () -> { TimeUnit.SECONDS.sleep(1); threadSleepSample.changeReady(); return null; }; public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); executorService.submit(addCountTask); executorService.submit(addCountTask); executorService.submit(changeReadyTask); executorService.shutdown(); } // モニタロックを取得する。(*1) private synchronized void addCount() throws InterruptedException { while (!ready) { System.out.println(Thread.currentThread() + " is waiting."); // 状態待ちを行う。(*2) wait(); System.out.println(Thread.currentThread() + " is not waiting."); } count += 10; System.out.println(count); } // モニタロックを取得する。(*1) private synchronized void changeReady() { ready = true; // 状態変更通知を行う。(*3) notifyAll(); } }
実行結果は以下の通りです。
Thread[pool-1-thread-2,5,main] is waiting. Thread[pool-1-thread-1,5,main] is waiting. Thread[pool-1-thread-2,5,main] is not waiting. 10 Thread[pool-1-thread-1,5,main] is not waiting. 20
解説
モニタロックの取得(*1)
モニタロックとは
モニタロックとは、各インスタンスに1つ用意されているロックのことです。排他制御にモニタロックを用いることで、あるインスタンスにおける特定処理の実行を、単一スレッドのみに制限しています。
モニタロックの取得方法
以下の通り記載することで、該当のメソッドを実行する前に、モニタロックの取得を試みます。
アクセス修飾子 synchronized 戻り値の型 メソッド名(...) {
実装例においては、threadSleepSample
インスタンスのaddCount
メソッド, changeReady
メソッドの実行は、モニタロックを取得した単一スレッドのみが可能です。
状態待ち(*2)
wait
メソッドを呼び出し、期待する状態待ちを行います。
sleep
メソッドでなく、wait
メソッドを用いる理由
処理を停止するなら、sleep
メソッドでも可能と考えるかもしれません。
ですが、sleep
メソッドには以下特徴があります。
- モニタロックをスレッドが保持し続ける。(状態更新にモニタロック取得が必須の場合、デッドロックになる。)
- 期待した状態が満たされたか否かに関わらず、指定した時間停止する。
一方で、wait
メソッドには以下特徴があります。
- モニタロックをスレッドが手放す。
- 期待した状態が満たされた可能性の通知(後述の状態変更通知を御覧下さい。)を受け取るまで、停止する。
上記をまとめると、両者の使い分けは以下の通りです。
sleep
メソッド: 1つのスレッドにおける、処理間隔の調整に用いる。wait
メソッド: マルチスレッドにおける、状態待ちに用いる。
wait
をwhile
文で囲う理由
wait
で停止したスレッドは、後述の状態変更通知を受けて処理を再開します。
しかし、ここで問題になるのが、状態変更通知を受けても目的の状態が満たされたとは限らないということです。
というのも、状態変更通知はどの変数がどの様な状態になったか、という情報を含みません。例えば、異なる変数の状態待ちを行うスレッドが複数ある場合、その何れかが状態変更通知を受け取っても、それが自身が待ちを行っている変数に関する通知とは限らないわけです。
その為、wait
メソッドをwhile
文で囲うことで、処理が再開した後に再度期待する状態かチェックし、そうでない場合はもう一度待ちを行います。
状態変更通知(*3)
notifyAll
メソッドを呼び出し、状態待ちを行っているスレッドに、期待した状態が満たされた可能性を通知します。
notify
メソッドでなく、notifyAll
メソッドである理由
以下にnotify
メソッドとnotifyAll
メソッドの違いをまとめました。
説明に当たり、以下用語を用います。
- ウェイトセット:
wait
メソッドを呼び出し、状態待ちを行うスレッドの集合です。 - エントリセット: 状態変更通知を受けて、モニタロックの取得を試みるスレッドの集合です。
各メソッドの挙動の違い
notify
メソッド
下図の通りの挙動になります。notifyAll
メソッドとの違いは、1つのスレッドがウェイトセットからエントリセットに移動する点です。
notifyAll
メソッド
下図の通りの挙動になります。notify
メソッドとの違いは、全てのスレッドがウェイトセットからエントリセットに移動する点です。
notify
メソッドの問題点
状態変更通知にnotify
メソッドを利用した場合、1つのスレッドのみがウェイトセットからエントリセットに移動する為、以下の問題点があります。
- 移動したスレッドが該当の通知を待っていたスレッドではなかった場合、その通知を待っていた他のスレッドが起動する機会を永遠に失う可能性がある。
- 選ばれなかった他のスレッドにモニタロック取得の機会が与えられず、スレッド毎の処理時間のバラつきが大きくなる。