概要
スレッドが絡む不具合は、再現させることがいちばん難しい――本番でだけ重なるタイミングは、テストでは一度も出ないものです。だからこそ、start() と run() の違い、join のタイムアウト、interrupt の流儀といった基本を最初に正確に押さえておくことが、後のデバッグ時間を大きく左右します。Java でスレッドを作る方法は Thread 継承・Runnable 実装・ラムダ式の3パターンあり、どれを選ぶかで保守性とテストのしやすさが変わります。この記事ではスレッドの生成から start/join による制御、interrupt による安全な中断までを、実行して動きを確かめられるコードで整理します。Java 21 の仮想スレッド(Virtual Threads)との違いにも触れ、今後の選択の判断材料を示します。
使いどころ
バッチ処理で複数ファイルの取り込みを並列化し、全スレッドの完了を join で待ち合わせる
ログ出力や通知送信を別スレッドに委譲し、メイン処理のレスポンスタイムを短縮する
タイムアウト付きの外部API呼び出しを別スレッドで実行し、interrupt で中断可能にする
コード例
public class ThreadBasicDemo {
// Runnable をラムダ式で記述(推奨パターン)
public static void main(String[] args) throws InterruptedException {
// パターン1: ラムダ式で Runnable を直接書く
var t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] count: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // フラグを再セット
System.out.println(Thread.currentThread().getName() + ": 中断");
return;
}
}
}, "worker-1");
var t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] count: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "worker-2");
t1.start(); // 新スレッドで実行開始
t2.start();
t1.join(); // t1 の完了を待つ
t2.join(); // t2 の完了を待つ
System.out.println("全スレッド完了");
// interrupt: 眠っているスレッドを安全に止める
var sleeper = new Thread(() -> {
try {
Thread.sleep(60_000); // 外部応答待ちを想定した長い sleep
System.out.println("ここには到達しない");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // フラグを再セットしてから抜ける
System.out.println("[sleeper] 割り込みで中断");
}
}, "sleeper");
sleeper.start();
sleeper.interrupt(); // sleep 中のスレッドには即座に InterruptedException が飛ぶ
sleeper.join(1000); // タイムアウト付き join。相手が固まっても無限待ちにならない
System.out.println("sleeper 生存: " + sleeper.isAlive()); // false
// スレッド情報の確認
var current = Thread.currentThread();
System.out.println("メインスレッド名: " + current.getName());
System.out.println("デーモン: " + current.isDaemon());
}
}Version Coverage
var による型推論と record で Runnable を実装するパターンが加わり、コードが簡潔になる。
// Java 17: ラムダ式 + var で簡潔に
var t = new Thread(() -> {
System.out.println("Hello from " +
Thread.currentThread().getName());
}, "worker-1");
t.start();
t.join();Library Comparison
注意点
start() ではなく run() を直接呼ぶとメインスレッドで同期実行される。新規スレッドは生成されないため、並行処理にならない
InterruptedException を catch したら Thread.currentThread().interrupt() で割り込みフラグを再セットするのが原則。握りつぶすとキャンセル伝播が途切れる
Thread を継承するとそのクラスは他のクラスを継承できなくなる。Runnable 実装やラムダ式のほうが拡張性が高い
スレッド名を設定しておかないと、ログやスレッドダンプでの特定が困難になる。new Thread(task, "worker-1") のように明示する
join() にタイムアウトを設定しないと、相手スレッドが終了しない限りメインスレッドが永久にブロックされる
実務では run() を直接呼ぶ誤りや、スレッド名を設定しないためスレッドダンプで特定できないケースが多い。命名と start/join の基本を最初に押さえておくと、後のデバッグが楽になる。
FAQ
Runnable 実装(またはラムダ式)を推奨します。Java は単一継承のため、Thread を継承すると他のクラスを継承できなくなります。テストも Runnable のほうが書きやすいです。
I/O バウンドな処理には適していますが、CPU バウンドな処理では従来のプラットフォームスレッドが有利です。用途に応じて使い分ける必要があります。
start() は新しいスレッドを作成してその中で run() を呼びます。run() を直接呼ぶと呼び出し元のスレッドで同期実行されるため、並行処理になりません。