java-recipes

ホーム マルチスレッド › Th-01

Th-01: Thread の基本

Java でスレッドを起動する方法は大きく2つあります。Thread クラスを継承する方法と、Runnable インターフェースを実装して Thread に渡す方法です。 それぞれの使い方と、start() / run() の違い、join() による待機を理解しましょう。

Thread クラス継承 vs Runnable 実装

スレッドの処理内容を定義するには2つの方法があります。Runnable インターフェースを実装する方法が推奨です。 Java は単一継承(1つのクラスしか継承できない)のため、Thread を継承してしまうと他のクラスを継承できなくなります。Runnable はインターフェースなので、既存の継承関係に影響しません。

方法書き方推奨度備考
Thread クラスを継承class MyThread extends Thread他のクラスを継承できなくなる
Runnable を実装class MyTask implements Runnable柔軟性が高く、ExecutorService にも渡せる
ラムダ式(Java 8+)new Thread(() -> { ... })短い処理に向く。Runnable を匿名で書ける

start() と run() の違い

スレッドを起動するには必ず start() を呼びます。run() を直接呼ぶと、新しいスレッドは起動されずメインスレッドで処理が実行されます。

Thread t = new Thread(task);
t.start(); // ✅ 新しいスレッドで run() を実行
t.run();   // ❌ メインスレッドで run() を実行(スレッドが増えない)

join() による完了待機

join() を呼ぶと、そのスレッドが終了するまでメインスレッドが待機します。join() なしで処理を続けると、スレッドの完了前にプログラムが終了してしまう場合があります。

サンプルコード

Sample.java
public class ThreadBasicSample {

    // パターン1: Thread クラスを継承
    static class CounterThread extends Thread {
        private final String name;
        private final int count;

        public CounterThread(String name, int count) {
            super(name); // スレッド名を設定
            this.name = name;
            this.count = count;
        }

        @Override
        public void run() {
            for (int i = 1; i <= count; i++) {
                System.out.println(name + ": " + i);
                try {
                    Thread.sleep(100); // 100ms 待機
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // interrupt フラグを再セット
                    System.out.println(name + ": 中断されました");
                    return;
                }
            }
        }
    }

    // パターン2: Runnable インターフェースを実装(推奨)
    static class PrintTask implements Runnable {
        private final String message;
        private final int repeat;

        public PrintTask(String message, int repeat) {
            this.message = message;
            this.repeat = repeat;
        }

        @Override
        public void run() {
            for (int i = 0; i < repeat; i++) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + message);
                try {
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== パターン1: Thread クラスを継承 ===");
        CounterThread t1 = new CounterThread("スレッドA", 3);
        CounterThread t2 = new CounterThread("スレッドB", 3);
        t1.start(); // 新しいスレッドで run() を実行
        t2.start();
        t1.join(); // スレッド終了まで待機
        t2.join();

        System.out.println("\n=== パターン2: Runnable を実装(推奨)===");
        Thread t3 = new Thread(new PrintTask("Hello from Runnable", 3), "worker-1");
        Thread t4 = new Thread(new PrintTask("Hello from Runnable", 3), "worker-2");
        t3.start();
        t4.start();
        t3.join();
        t4.join();

        System.out.println("\n=== よくある間違い: run() を直接呼ぶ ===");
        Thread badThread = new Thread(new PrintTask("直接 run() 呼び出し", 2), "bad-thread");
        badThread.run(); // start() ではなく run() を直接呼ぶとメインスレッドで実行される
        System.out.println("run() 直接呼び出しはメインスレッドで実行される");

        System.out.println("\n=== スレッド情報 ===");
        Thread current = Thread.currentThread();
        System.out.println("スレッド名: " + current.getName());
        System.out.println("スレッドID: " + current.getId());
        System.out.println("優先度: " + current.getPriority());
        System.out.println("デーモン: " + current.isDaemon());
    }
}

よくあるミス・注意点

⚠️ thread.run() を呼んでしまう

run() を直接呼ぶと新しいスレッドは起動されません。メインスレッド上でそのまま実行されるため、 並行処理にならず想定外の動作になります。スレッドを起動するときは必ず start() を使ってください。

⚠️ join() を忘れてメインスレッドが先に終了する

スレッドを起動した後に join() を呼ばないと、スレッドの処理が完了する前にメインスレッドが終了してしまうことがあります。 スレッドの結果を使う処理の前に必ず t.join() を呼びましょう。

⚠️ InterruptedException を握りつぶす

Thread.sleep() などが投げる InterruptedException を catch した後、何もしないまま続けるのは危険です。Thread.currentThread().interrupt() を呼んで interrupt フラグを再セットしてください。 これをしないと、外部からのスレッド中断シグナルが失われます。

テストする観点

  • start() を呼ぶと新しいスレッドが起動され、Thread.currentThread().getName() がメインスレッドと異なること
  • run() を直接呼んだ場合、スレッド名がメインスレッドと同じであること(シングルスレッドで実行されている)
  • join() を呼ぶとスレッドの処理が完全に終了してから次の行が実行されること
  • InterruptedException を catch した後、isInterrupted()true であること
  • ✅ Java 21 では Thread.ofVirtual().start() で起動したスレッドの isVirtual()true であること

GitHub でソースコードを見る →