概要
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("全スレッド完了");
// スレッド情報の確認
var current = Thread.currentThread();
System.out.println("メインスレッド名: " + current.getName());
System.out.println("デーモン: " + current.isDaemon());
System.out.println("優先度: " + current.getPriority());
}
}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() にタイムアウトを設定しないと、相手スレッドが終了しない限りメインスレッドが永久にブロックされる
FAQ
Runnable 実装(またはラムダ式)を推奨します。Java は単一継承のため、Thread を継承すると他のクラスを継承できなくなります。テストも Runnable のほうが書きやすいです。
I/O バウンドな処理には適していますが、CPU バウンドな処理では従来のプラットフォームスレッドが有利です。用途に応じて使い分ける必要があります。
start() は新しいスレッドを作成してその中で run() を呼びます。run() を直接呼ぶと呼び出し元のスレッドで同期実行されるため、並行処理になりません。