1. Java におけるスレッドの基本状態
Java のスレッドは単に「起動した」か「停止した」かだけではありません。スレッドにはライフサイクルがあり、各段階で振る舞いが異なります。これらの段階を理解することは、安定したマルチスレッドアプリケーションを書くことや、不可解な「ハング」や「予期せぬ終了」をデバッグするうえで鍵となります。
どんな状態がある?
Java では次の基本状態が定義されています(列挙型 Thread.State に列挙されています)。
| 状態 | 説明 |
|---|---|
|
スレッドは作成されたが、まだ起動していない(new Thread(...) はあるが、start() は未呼び出し) |
|
実行可能、または現在実行中 |
|
モニターの解放を待っている(synchronized ブロックでブロックされている) |
|
別のスレッドに「起こして」もらうのを待っている(たとえば Object.wait()) |
|
タイムアウト付きで待機している(例: Thread.sleep(1000)、wait(1000)、join(1000)) |
|
スレッドの実行が終了した |
豆知識:
古い書籍や記事では別の名称や少し異なる図を見かけることがあります。ですが Java 5+ 以降、これらの状態が標準とされています。
スレッドライフサイクルの可視化
stateDiagram-v2
[*] --> NEW
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: synchronized に入ろうとしたがモニターが使用中
BLOCKED --> RUNNABLE: モニターが解放された
RUNNABLE --> WAITING: wait(), join()
RUNNABLE --> TIMED_WAITING: sleep(), wait(timeout), join(timeout)
WAITING --> RUNNABLE: notify()/notifyAll(), join() が終了
TIMED_WAITING --> RUNNABLE: タイムアウト満了 / notify()/notifyAll()
RUNNABLE --> TERMINATED: run() が終了
WAITING --> TERMINATED: run() が終了(まれ)
TIMED_WAITING --> TERMINATED: run() が終了(まれ)
2. スレッドを制御するメソッド
スリープ: Thread.sleep(long millis)
ときには、他のスレッドの邪魔をしないためやイベントを待つためにスレッドを「眠らせる」必要があります。メソッド Thread.sleep(ms) はスレッドを指定ミリ秒間、状態
TIMED_WAITING に移します。
System.out.println("スレッドは 2 秒間スリープします...");
Thread.sleep(2000); // 2秒間スリープ
System.out.println("スレッドが起きました!");
- スリープが終わると、スレッドは状態
(実行可能)に戻ります。RUNNABLE - スリープ中にスレッドが割り込まれると、InterruptedException がスローされます。
他のスレッドの終了を待つ: join()
単にスレッドを起動するだけでなく、処理が終わるのを待ちたいことがあります。そのために Java には join() というメソッドがあります:
Thread t = new Thread(() -> {
System.out.println("作業中...");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("完了!");
});
t.start();
System.out.println("スレッド t の終了を待っています...");
t.join(); // 現在のスレッド(例: main)は t を待つ
System.out.println("スレッド t が終了しました!");
ここでは、join() を呼び出した瞬間にメインスレッドは待機を開始します。この間、状態は
WAITING で、スレッド
t が終了するのを待ちます。タイムアウト付きの
join(long millis) もあり、その場合は状態が
TIMED_WAITING になります。
スレッドの割り込み: interrupt()
ときには、たとえばユーザーが「キャンセル」を押した場合など、スレッドに丁寧に終了してもらいたいことがあります。そのために interrupt() メソッドを使います:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 作業中...
}
System.out.println("割り込みによりスレッドを終了!");
});
t.start();
// ... しばらくして:
t.interrupt(); // スレッドへのシグナル: 「そろそろ停止して」
重要なのは、interrupt() の呼び出しはスレッドを即座に終了させるわけではないという点です。これは単に特別なフラグを立てるだけです。スレッド自身が isInterrupted() でこのフラグを適宜確認し、自発的に終了する必要があります。もしスレッドがその時点でスリープ中(sleep())や待機中(wait())であれば、フラグが立つだけでなく、直ちに InterruptedException が送出されます。これが Java における「正しい」スレッド停止の方法です。プログラムが力ずくで止めるのではなく、終了すべきことを通知するのです。
3. 状態間の遷移の例
例を通して、スレッドがどのように状態を「旅する」かを見てみましょう。
例 1: NEW
→ RUNNABLE
→ TERMINATED
NEWRUNNABLETERMINATEDThread t = new Thread(() -> System.out.println("こんにちは!"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE(またはスレッドが非常に速ければ TERMINATED)
t.join();
System.out.println(t.getState()); // TERMINATED
例 2: RUNNABLE
→ TIMED_WAITING
→ RUNNABLE
→ TERMINATED
RUNNABLETIMED_WAITINGRUNNABLETERMINATEDThread t = new Thread(() -> {
try {
System.out.println("スリープに入ります...");
Thread.sleep(1000); // TIMED_WAITING
System.out.println("起きました!");
} catch (InterruptedException e) {
System.out.println("スレッドが割り込まれました!");
}
});
t.start();
例 3: RUNNABLE
→ WAITING
(join())
RUNNABLEWAITINGThread t1 = new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
System.out.println("t1 が終了");
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // t2 は t1 を待っており、WAITING にある
System.out.println("t2 は t1 を待ち終えた");
} catch (InterruptedException ignored) {}
});
t1.start();
t2.start();
例 4: BLOCKED
BLOCKEDObject lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
System.out.println("t1 は synchronized ブロックを抜けた");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 は synchronized に入った");
}
});
t1.start();
Thread.sleep(100); // t1 に lock を取得させる
t2.start();
Thread.sleep(100); // t2 に synchronized へ入ろうとさせる
System.out.println("t2 の状態: " + t2.getState()); // BLOCKED
4. スレッドの状態を知るには? メソッド isAlive() と getState()
- isAlive() — スレッドが起動済みで、まだ終了していない場合に true を返します(
はすでに false)。TERMINATED - getState() — 現在のスレッド状態(列挙型 Thread.State の値)を返します。
Thread t = new Thread(() -> {});
System.out.println(t.isAlive()); // false (NEW)
t.start();
System.out.println(t.isAlive()); // true (RUNNABLE/WAITING/...)
t.join();
System.out.println(t.isAlive()); // false (TERMINATED)
5. なぜスレッドを直接「kill」できないのか、そして実践的な助言
「kill」メソッドはない!
Java には、命令一つでスレッドを「殺す」メソッドは存在しません。なぜでしょうか? それは安全ではないからです。スレッドが何らかのリソース(ファイル、接続、ロック)を保持している最中に強制終了すると、システムを不整合な状態にしてしまう可能性があるからです。
非推奨のメソッド: stop()、suspend()、resume()
古いバージョンの Java には stop()、suspend()、resume() といったメソッドがありました。現在は @Deprecated としてマークされており、使用は強く非推奨です。理由は次のとおりです。
- stop() は任意の時点でスレッドを殺してしまうため、データを不整合な状態に残し得ます。
- suspend() はロックを保持したままスレッドを「凍結」させる可能性があり、その結果プログラム全体がハングします。
- resume() は、対象スレッドがすでに終了していると「解凍」できないことがあります。
現代的なアプローチ:
スレッドは、割り込みフラグ(isInterrupted())やその他のシグナルに反応して自ら正しく終了すべきです。
ベストプラクティス
- 非推奨とマークされたメソッドは呼び出さない。
- スレッドの停止には割り込みフラグを使う(interrupt() と isInterrupted() の確認)。
- デバッグ時はスレッドの状態を確認する—ハングやデッドロックの発見に役立ちます。
- スレッドの終了を待つ必要があるときは join() を忘れない。
6. スレッドライフサイクルでありがちなミス
エラー 1: 同じスレッドを再度起動しようとする。
Java ではスレッドは一度しか起動できません。start() を 2 回呼ぶと IllegalThreadStateException になります。処理を繰り返したい場合は新しい Thread オブジェクトを作成してください。
Thread t = new Thread(() -> {});
t.start();
t.start(); // 例外がスローされる!
エラー 2: run() と start() を混同する。
run() を直接呼んでも新しいスレッドでは実行されません—現在のスレッド(たとえば main)で実行されます。新しいスレッドを本当に起動するのは start() だけです。
エラー 3: InterruptedException を処理しない。
スレッドがスリープや待機中に割り込まれると、InterruptedException が投げられます。これを無視すると、スレッドが「永遠に眠り続ける」か、予期せず終了することがあります。
エラー 4: スレッドの状態を確認しない。
あるスレッドが別のスレッドを待っているのに、その相手がすでに終了していたり決して開始されないせいでプログラムがハングすることがあります。診断には getState() と isAlive() を使いましょう。
エラー 5: 古いスレッド制御メソッドを使う。
stop()、suspend()、resume() は有害です。たとえ「手っ取り早く直したい」と思っても使わないでください。
GO TO FULL VERSION