Javaのスレッドは、Runnable(やりたい処理)を Thread に渡して start() を呼ぶと作れます。 いちばん短い形は new Thread(() -> 処理).start(); の1行です。スレッドとは、1つのプログラムの中で同時に進む「処理の流れ」のことで、main メソッドも main という名前のスレッドの上で動いています。

この記事では、スレッドの作り方5通りの比較、run() と start() の違い、join の有無で出力が変わる例、sleep と interrupt、InterruptedException の扱い方、デーモンスレッド、スレッド名と優先度までを扱います。掲載したコードはすべて OpenJDK 21.0.10 で実行し、出力をそのまま載せています。Java 25 での違いは公式の API ドキュメントと JEP で確認して注記しました。

Javaスレッドの早見表

作り方5通りの比較

作り方 書き方の例 戻り値 向いている場面
1. Thread を継承 class MyThread extends Thread で run() をオーバーライド なし 仕組みを学ぶとき。ほかのクラスを継承できなくなる
2. Runnable を実装 new Thread(new MyTask()).start() なし 処理(タスク)とスレッドを分けたいとき
3. ラムダ式 new Thread(() -> 処理).start() なし 短い処理を1本だけ動かすとき。いちばん手軽
4. ExecutorService pool.submit(() -> 処理) Future で受け取れる タスクが複数あるとき。スレッドを使い回せる
5. 仮想スレッド(Java 21〜) Thread.ofVirtual().start(() -> 処理) なし(Executor 経由なら可) 待ち時間の多い処理を大量に動かすとき

よく使うメソッド

やりたいこと メソッド ポイント
スレッドを開始する t.start() 新しいスレッドが作られる。1つのスレッドにつき1回だけ
終わるまで待つ t.join() 呼んだ側のスレッドが待つ
時間を区切って待つ t.join(Duration.ofMillis(300)) 終了したかどうかが boolean で返る(Java 19〜)
一定時間止める Thread.sleep(1000) 止まるのは自分のスレッド。ミリ秒指定
止めてほしいと伝える t.interrupt() 強制終了ではなく「お願い」。相手が応じて終わる
割り込まれたか調べる Thread.currentThread().isInterrupted() フラグを変えずに確認
今のスレッドを取得 Thread.currentThread() 名前の確認によく使う
名前を付ける new Thread(task, "名前") / t.setName("名前") ログやスレッドダンプで見分けられる
デーモンにする t.setDaemon(true) start() の前に呼ぶ
状態を調べる t.getState() / t.isAlive() NEW、RUNNABLE、TIMED_WAITING、TERMINATED など

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaスレッドの作り方|Thread・Runnable・sleep・join」

スレッドの作り方5通りを1つのファイルで動かす

5通りの作り方を並べたコードです。FiveWays.java に保存して、そのまま実行できます。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FiveWays {
    // 1. Thread を継承して run() をオーバーライド
    static class MyThread extends Thread {
        @Override public void run() { say("1. Thread継承"); }
    }
    // 2. Runnable を実装して Thread に渡す
    static class MyTask implements Runnable {
        @Override public void run() { say("2. Runnable実装"); }
    }
    static void say(String label) {
        Thread t = Thread.currentThread();
        System.out.printf("%-18s name=%-12s virtual=%s%n", label, t.getName().isEmpty() ? "(なし)" : t.getName(), t.isVirtual());
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        t1.start(); t1.join();

        Thread t2 = new Thread(new MyTask());
        t2.start(); t2.join();

        // 3. ラムダ式(Runnable は関数型インターフェース)
        Thread t3 = new Thread(() -> say("3. ラムダ式"), "lambda-1");
        t3.start(); t3.join();

        // 4. ExecutorService(スレッドプール)。戻り値も受け取れる
        try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
            Future<Integer> f = pool.submit(() -> { say("4. ExecutorService"); return 42; });
            System.out.println("   戻り値 = " + f.get());
        }

        // 5. 仮想スレッド(Java 21〜)
        Thread t5 = Thread.ofVirtual().name("virtual-1").start(() -> say("5. 仮想スレッド"));
        t5.join();
        Thread t6 = Thread.startVirtualThread(() -> say("5'. 名前なし仮想"));
        t6.join();
    }
}

実行コマンドと結果(Java 21.0.10)です。以降のコードも同じ手順で動かしています。

javac -encoding UTF-8 FiveWays.java
java FiveWays
1. Thread継承        name=Thread-0     virtual=false
2. Runnable実装      name=Thread-1     virtual=false
3. ラムダ式            name=lambda-1     virtual=false
4. ExecutorService name=pool-1-thread-1 virtual=false
   戻り値 = 42
5. 仮想スレッド          name=virtual-1    virtual=true
5'. 名前なし仮想         name=(なし)         virtual=true

結果から次のことがわかります。

  • 名前を付けないスレッドは Thread-0、Thread-1 と連番になります。 スレッドプールのスレッドは pool-1-thread-1 の形です。
  • 仮想スレッドは、名前を付けないと空文字になります(isVirtual() は true)。
  • ExecutorService は Java 19 から AutoCloseable になっているので、try-with-resources で囲むと、タスクの完了を待ってから自動で閉じてくれます。

5通りの選び方

状況 選ぶもの 理由
短い処理を1本だけ別に動かしたい ラムダ式 + Thread 1行で書ける
処理をクラスとして再利用・テストしたい Runnable を実装 処理とスレッドを分けられる
タスクが複数ある、結果を受け取りたい ExecutorService スレッドを使い回せる。Future で戻り値・例外を受け取れる
通信やファイルなど待ち時間の多い処理が大量 仮想スレッド 1タスク1スレッドの書き方のまま大量に動かせる
スレッドの仕組みを学びたい Thread を継承 run() と start() の関係が見えやすい

Thread を継承する方法は、Javaが単一継承のため、ほかのクラスを継承できなくなります。 処理を Runnable(またはラムダ式)として切り出しておくと、同じ処理を Thread にも ExecutorService にも仮想スレッドにも渡せます。

ExecutorService の種類や shutdown の手順は別記事「Java ExecutorServiceの使い方|スレッドプールと仮想スレッド」、仮想スレッドの性能や使いどころは「Java 仮想スレッド入門|10万タスク実測と使いどころ判断表」で詳しく扱っています。この記事では、その土台になる Thread そのものの動きに絞ります。

run()とstart()の違い

スレッドを学び始めたときに間違えやすいのが、run() を直接呼ぶ書き方です。run() はただのメソッド呼び出しで、新しいスレッドは作られません。新しいスレッドを作るのは start() です。

public class RunVsStart {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> System.out.println("  実行しているスレッド: " + Thread.currentThread().getName());

        Thread a = new Thread(task, "worker-A");
        System.out.println("run() を直接呼ぶ");
        a.run();                 // ただのメソッド呼び出し。新しいスレッドは作られない
        System.out.println("  呼んだ後の状態: " + a.getState());

        Thread b = new Thread(task, "worker-B");
        System.out.println("start() を呼ぶ");
        b.start();               // 新しいスレッドが作られ、その中で run() が動く
        b.join();
        System.out.println("  終了後の状態: " + b.getState());

        System.out.println("同じスレッドで start() を2回呼ぶ");
        try {
            b.start();
        } catch (IllegalThreadStateException e) {
            System.out.println("  " + e);
        }
    }
}

実行結果(Java 21.0.10):

run() を直接呼ぶ
  実行しているスレッド: main
  呼んだ後の状態: NEW
start() を呼ぶ
  実行しているスレッド: worker-B
  終了後の状態: TERMINATED
同じスレッドで start() を2回呼ぶ
  java.lang.IllegalThreadStateException

a.run() のほうは、実行しているスレッドが main のままで、worker-A の状態は NEW(まだ開始していない)です。start() を呼んだ worker-B だけが、自分の名前のスレッドで動いています。

時間で確かめる

1秒かかる処理を2つ、run() と start() で動かして所要時間を比べます。

public class Blocking {
    static void work(String name) {
        try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        System.out.println("  " + name + " 完了 by " + Thread.currentThread().getName());
    }
    public static void main(String[] args) throws InterruptedException {
        long s = System.currentTimeMillis();
        Thread a = new Thread(() -> work("A"), "worker-A");
        Thread b = new Thread(() -> work("B"), "worker-B");
        System.out.println("run() で順に実行");
        a.run(); b.run();
        System.out.println("  かかった時間: 約" + (System.currentTimeMillis() - s) / 1000 + "秒");

        s = System.currentTimeMillis();
        Thread c = new Thread(() -> work("C"), "worker-C");
        Thread d = new Thread(() -> work("D"), "worker-D");
        System.out.println("start() で並行に実行");
        c.start(); d.start();
        c.join(); d.join();
        System.out.println("  かかった時間: 約" + (System.currentTimeMillis() - s) / 1000 + "秒");
    }
}
run() で順に実行
  A 完了 by main
  B 完了 by main
  かかった時間: 約2秒
start() で並行に実行
  D 完了 by worker-D
  C 完了 by worker-C
  かかった時間: 約1秒

run() では2秒、start() では約1秒でした。また、start() のほうは C より先に D が完了しています。スレッドがどの順で動くかは OS のスケジューラが決めるため、実行のたびに入れ替わることがあります。 順番に意味がある処理は、次の join などで自分でそろえます。

run() を直接呼ぶ start() を呼ぶ
新しいスレッド 作られない 作られる
処理が動くスレッド 呼び出した側(例:main) 新しいスレッド
呼び出し側 処理が終わるまで待つ すぐ次の行へ進む
呼べる回数 何度でも 1回だけ(2回目は IllegalThreadStateException)

同じ処理をもう一度動かしたいときは、Thread を新しく作り直すか、ExecutorService に同じ Runnable を再度渡します。

joinでスレッドの終了を待つ

start() を呼んだ側は、スレッドの終了を待たずに次の行へ進みます。結果がそろってから次の処理をしたいときは join() で待ちます。

joinの有無で出力が変わる例

3つのスレッドがそれぞれ 100、200、300 を配列に入れ、main が合計を出すコードです。

import java.util.ArrayList;
import java.util.List;

public class JoinDemo {
    static int[] results = new int[3];

    static List<Thread> startWorkers() {
        results = new int[3];
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            final int no = i;
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(100 * (no + 1));      // 重い処理のつもり
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
                results[no] = (no + 1) * 100;
            }, "worker-" + no);
            t.start();
            threads.add(t);
        }
        return threads;
    }

    public static void main(String[] args) throws InterruptedException {
        // join なし:結果がそろう前に合計してしまう
        List<Thread> ts = startWorkers();
        System.out.println("joinなし: 合計 = " + (results[0] + results[1] + results[2]));
        for (Thread t : ts) t.join();                  // 次の実験の前に終わらせておく

        // join あり:全スレッドの終了を待ってから合計する
        ts = startWorkers();
        for (Thread t : ts) t.join();
        System.out.println("joinあり: 合計 = " + (results[0] + results[1] + results[2]));

        // 時間を区切って待つ
        Thread slow = new Thread(() -> {
            try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("slow: 割り込みで終了"); }
        }, "slow");
        slow.start();
        boolean finished = slow.join(java.time.Duration.ofMillis(300));   // Java 19〜
        System.out.println("300ms待った結果: 終了した=" + finished + ", isAlive=" + slow.isAlive());
        slow.interrupt();
        slow.join();
        System.out.println("最終状態: " + slow.getState());
    }
}

実行結果(Java 21.0.10):

joinなし: 合計 = 0
joinあり: 合計 = 600
300ms待った結果: 終了した=false, isAlive=true
slow: 割り込みで終了
最終状態: TERMINATED

join なしでは合計が 0、join ありでは 600 になりました。 join なしの場合、3つのスレッドがまだ sleep している間に main が合計を計算しています。エラーは出ないので、「たまに値が入らない」という形で現れます。

join には3つの形があります。

書き方 動き 戻り値
t.join() t が終わるまで待つ なし
t.join(1000) 最大1000ミリ秒待つ(0 は無期限) なし。終わったかは t.isAlive() で確認
t.join(Duration.ofSeconds(1)) 最大1秒待つ(Java 19〜) 終了していれば true
  • 複数のスレッドを待つときは、「全部 start() してから、全部 join() する」順に書きます。 start() の直後に join() を書くと、1本ずつ順番に動かすのと同じになります。
  • join() を別のスレッドで動かしている処理の結果と組み合わせるときは、戻り値を受け取れる ExecutorService の Future.get() や CompletableFuture のほうが書きやすくなります。
  • 上の例は、スレッドごとに別の要素(results[0]〜results[2])へ書いています。複数のスレッドが同じ変数を書き換える場合は排他制御が必要で、別記事「Java synchronizedで排他制御|スレッドセーフを実行結果で検証」にまとめています。

sleepでスレッドを一定時間止める

Thread.sleep() は、呼び出したスレッド自身を指定した時間だけ止めます。

書き方 意味
Thread.sleep(1000) 1000ミリ秒(1秒)止める
Thread.sleep(Duration.ofSeconds(1)) 1秒止める(Java 19〜。単位が読み取りやすい)
TimeUnit.SECONDS.sleep(1) 1秒止める(java.util.concurrent.TimeUnit)

押さえておきたい点は3つです。

  • sleep は static メソッドです。 t.sleep(1000) と書いても、止まるのは t ではなく、その行を実行しているスレッドです。Thread.sleep(...) とクラス名で書くと誤解がありません。
  • sleep は検査例外 InterruptedException を投げるので、try-catch で囲むか、メソッドに throws InterruptedException を付けます。Runnable.run() には throws を付けられないため、ラムダ式の中では try-catch が必要です。
  • sleep 中も、synchronized で取ったロックは持ったままです。ロックを手放して待つ wait() とは動きが違います。

interruptとInterruptedExceptionの正しい扱い

interruptは「止めてほしい」という合図

スレッドを外から止めたいときは interrupt() を使います。interrupt() は強制終了ではなく、相手のスレッドに「割り込みフラグ」を立てる合図です。 相手がその合図を見て、自分で処理を終えます。

相手のスレッドの状態 interrupt() を呼ぶとどうなるか
sleep・join・wait で待っている 待ちが解除され、InterruptedException が投げられる。フラグはクリアされる
計算など普通の処理をしている フラグが立つだけ。自分で isInterrupted() を確認する必要がある

2つのパターンを実行して確かめます。

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        // パターン1:sleep 中に割り込まれる → InterruptedException で抜ける
        Thread sleeper = new Thread(() -> {
            try {
                for (int i = 1; ; i++) {
                    System.out.println("[sleeper] " + i + "回目の処理");
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                System.out.println("[sleeper] 例外を受け取った: " + e.getMessage());
                System.out.println("[sleeper] catch 内のフラグ: " + Thread.currentThread().isInterrupted());
            } finally {
                System.out.println("[sleeper] 後片付けをして終了");
            }
        }, "sleeper");
        sleeper.start();
        Thread.sleep(500);
        sleeper.interrupt();
        sleeper.join();

        // パターン2:sleep しない計算ループ → フラグを自分で確認する
        Thread counter = new Thread(() -> {
            long n = 0;
            while (!Thread.currentThread().isInterrupted()) {
                n++;
            }
            System.out.println("[counter] フラグを見て終了。数えた回数は " + (n > 0 ? "1以上" : "0"));
        }, "counter");
        counter.start();
        Thread.sleep(100);
        counter.interrupt();
        counter.join();
        System.out.println("[main] 2つとも終了: " + sleeper.getState() + " / " + counter.getState());
    }
}

実行結果(Java 21.0.10):

[sleeper] 1回目の処理
[sleeper] 2回目の処理
[sleeper] 3回目の処理
[sleeper] 例外を受け取った: sleep interrupted
[sleeper] catch 内のフラグ: false
[sleeper] 後片付けをして終了
[counter] フラグを見て終了。数えた回数は 1以上
[main] 2つとも終了: TERMINATED / TERMINATED

注目したいのは catch 内のフラグ: false です。InterruptedException が投げられた時点で、割り込みフラグはクリアされています。この性質が、次の「直す前と後」につながります。

直す前と後:catchで何もしないとループが止まらない

sleep を含む小さなメソッドを、ループから呼ぶ例です。catch の中身だけが違う2つを比べます。

public class Swallow {
    // 直す前:catch で何もしない
    static void pauseBad() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            // 何もしない
        }
    }
    // 直した後:フラグを立て直して呼び出し元に伝える
    static void pauseGood() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    static void runLoop(String label, Runnable pause) throws InterruptedException {
        Thread t = new Thread(() -> {
            int count = 0;
            while (!Thread.currentThread().isInterrupted() && count < 10) {
                pause.run();
                count++;
            }
            System.out.println(label + ": " + count + "回まわって終了");
        });
        t.start();
        Thread.sleep(120);
        t.interrupt();
        t.join();
    }

    public static void main(String[] args) throws InterruptedException {
        runLoop("直す前(何もしない)  ", Swallow::pauseBad);
        runLoop("直した後(フラグを戻す)", Swallow::pauseGood);
    }
}

実行結果(Java 21.0.10):

直す前(何もしない)  : 10回まわって終了
直した後(フラグを戻す): 3回まわって終了

120ミリ秒の時点で interrupt() を呼んでいますが、直す前のコードは合図が届かず、上限の10回まで回り続けました。 catch で例外を受けた時点でフラグが消え、ループの条件 isInterrupted() が false のままになるためです。直した後のコードは、Thread.currentThread().interrupt() でフラグを立て直しているので、3回で終了しています。

InterruptedExceptionの扱い方の判断表

状況 書き方 理由
メソッドに throws を付けられる throws InterruptedException でそのまま上に伝える いちばん素直。 呼び出し元が判断できる
Runnable.run() の中など throws を付けられない catch で Thread.currentThread().interrupt(); を呼んでから return フラグを戻して、外側のコードに割り込みを伝える
割り込みを受けて処理を終えるスレッドの最上位 catch で後片付けをして終了 そこで終わるので、フラグを戻さなくてもよい
別の例外に包みたい Thread.currentThread().interrupt(); の後に throw new IllegalStateException(e); 割り込みの事実と原因の両方を残す
catch を空にする/ログだけ出す 上のいずれかに書き換える 合図が消えて、スレッドが止まらなくなる

isInterrupted() と似た名前の Thread.interrupted() もあります。

メソッド 種類 フラグを消すか
t.isInterrupted() インスタンスメソッド 消さない
Thread.interrupted() static メソッド(現在のスレッドが対象) 消す
t.interrupt() インスタンスメソッド 立てる

スレッドを外から強制的に止める Thread.stop() は使わず、interrupt() と協調して終わる形にします。 Java 21 の stop() は呼ぶと UnsupportedOperationException を投げる仕様で、Java 25 の API ドキュメントでも「削除予定の非推奨(forRemoval=true)」のままです。また、Java 25 の Thread クラスには suspend()・resume() は載っていません(公式 API ドキュメントで確認)。

デーモンスレッド:mainが終わったら一緒に終わるスレッド

Javaのプログラム(JVM)は、デーモンでないスレッドがすべて終わったときに終了します。デーモンスレッドは「裏方」のスレッドで、残っていても JVM の終了を引き止めません。

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        boolean daemon = args.length > 0 && args[0].equals("daemon");
        Thread t = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("[bg] " + i + "回目");
                    Thread.sleep(300);
                }
                System.out.println("[bg] 最後まで終わった");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                System.out.println("[bg] finally");
            }
        }, "bg");
        t.setDaemon(daemon);          // start() の前に設定する
        t.start();
        Thread.sleep(500);
        System.out.println("[main] 終了。bg.isDaemon=" + t.isDaemon());
    }
}

通常のスレッド(java DaemonDemo)の実行結果:

[bg] 1回目
[bg] 2回目
[main] 終了。bg.isDaemon=false
[bg] 3回目
[bg] 4回目
[bg] 5回目
[bg] 最後まで終わった
[bg] finally

デーモンスレッド(java DaemonDemo daemon)の実行結果:

[bg] 1回目
[bg] 2回目
[main] 終了。bg.isDaemon=true

通常のスレッドは main が終わった後も最後まで動き、デーモンスレッドは main の終了とともに途中で終わりました。 デーモンのほうは finally の出力もありません。

通常のスレッド(ユーザースレッド) デーモンスレッド
設定 既定(main から作った場合) setDaemon(true) を start() の前に呼ぶ
JVM の終了 終わるまで JVM が待つ 待たない。途中でも終わる
finally 実行される JVM 終了時は実行されないことがある
向いている処理 ファイルへの書き込みなど、最後までやり遂げたい処理 定期的な監視・キャッシュの掃除など、途中で終わってよい処理

設定まわりの動きも確認しました。

public class DaemonErr {
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> { try { Thread.sleep(200); } catch (InterruptedException e) { } });
        t.start();
        try {
            t.setDaemon(true);        // start() の後に呼んだ
        } catch (IllegalThreadStateException e) {
            System.out.println("start後のsetDaemon: " + e);
        }
        Thread v = Thread.ofVirtual().unstarted(() -> { });
        System.out.println("仮想スレッドのisDaemon: " + v.isDaemon());
        try {
            v.setDaemon(false);
        } catch (IllegalArgumentException e) {
            System.out.println("仮想スレッドのsetDaemon(false): " + e);
        }
        v.setPriority(Thread.MAX_PRIORITY);
        System.out.println("仮想スレッドのsetPriority(10)後: " + v.getPriority());
    }
}
start後のsetDaemon: java.lang.IllegalThreadStateException
仮想スレッドのisDaemon: true
仮想スレッドのsetDaemon(false): java.lang.IllegalArgumentException: 'false' not legal for virtual threads
仮想スレッドのsetPriority(10)後: 5
  • setDaemon は start() の前に呼びます。 後から呼ぶと IllegalThreadStateException になります。
  • デーモンかどうかは、作った側のスレッドから引き継がれます。デーモンスレッドの中で作ったスレッドは、既定でデーモンになります。
  • 仮想スレッドは常にデーモンで、優先度も常に 5(NORM_PRIORITY)です。 仮想スレッドの終了を待ちたいときは join() するか、try-with-resources の ExecutorService を使います。

スレッド名・優先度・状態を確認する

スレッドに名前を付けておくと、ログやスレッドダンプで「どの処理のスレッドか」をすぐ見分けられます。

public class NameDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread main = Thread.currentThread();
        System.out.println("main: name=" + main.getName() + ", id=" + main.threadId() + ", priority=" + main.getPriority());

        Thread a = new Thread(() -> { });
        Thread b = new Thread(() -> { }, "report-writer");
        System.out.println("名前を付けない: " + a.getName());
        System.out.println("コンストラクタで: " + b.getName());
        a.setName("mail-sender");
        System.out.println("setNameで: " + a.getName());

        // Java 21〜:ビルダーで連番の名前を付ける
        Thread.Builder builder = Thread.ofPlatform().name("worker-", 1).daemon(false).priority(Thread.NORM_PRIORITY);
        Thread w1 = builder.unstarted(() -> { });
        Thread w2 = builder.unstarted(() -> { });
        System.out.println("ビルダー: " + w1.getName() + ", " + w2.getName());

        System.out.println("MIN=" + Thread.MIN_PRIORITY + " NORM=" + Thread.NORM_PRIORITY + " MAX=" + Thread.MAX_PRIORITY);
        b.setPriority(Thread.MAX_PRIORITY);
        System.out.println("setPriority後: " + b.getPriority());
        try {
            b.setPriority(11);
        } catch (IllegalArgumentException e) {
            System.out.println("範囲外: " + e);
        }
        System.out.println("toString: " + b);

        // 状態の移り変わり
        Thread s = new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) { }
        }, "state-demo");
        System.out.println("start前: " + s.getState() + ", isAlive=" + s.isAlive());
        s.start();
        Thread.sleep(50);
        System.out.println("sleep中: " + s.getState() + ", isAlive=" + s.isAlive());
        s.join();
        System.out.println("終了後: " + s.getState() + ", isAlive=" + s.isAlive());
    }
}

実行結果(Java 21.0.10):

main: name=main, id=1, priority=5
名前を付けない: Thread-0
コンストラクタで: report-writer
setNameで: mail-sender
ビルダー: worker-1, worker-2
MIN=1 NORM=5 MAX=10
setPriority後: 10
範囲外: java.lang.IllegalArgumentException
toString: Thread[#19,report-writer,10,main]
start前: NEW, isAlive=false
sleep中: TIMED_WAITING, isAlive=true
終了後: TERMINATED, isAlive=false

名前の付け方

方法 書き方 補足
コンストラクタ new Thread(task, "report-writer") いちばん手軽
後から変更 t.setName("mail-sender") 開始後でも変えられる
ビルダー(Java 21〜) Thread.ofPlatform().name("worker-", 1) worker-1、worker-2 と連番になる
仮想スレッドのビルダー Thread.ofVirtual().name("task-", 0) 同じ書き方で仮想スレッドを作れる

toString の Thread[#19,report-writer,10,main] は、順に スレッドID・名前・優先度・スレッドグループです。スレッドIDは threadId()(Java 19〜)で取得します。以前からある getId() は Java 19 で非推奨になっており、Java 25 の API ドキュメントでも非推奨のままです。

優先度は「目安」

優先度は 1(MIN_PRIORITY)〜10(MAX_PRIORITY)で、既定は 5(NORM_PRIORITY)です。範囲外を指定すると IllegalArgumentException になります。

優先度は OS のスケジューラへのヒントで、「高いほうが必ず先に動く」ことを約束するものではありません。 OS によって扱いが異なるため、処理の順番や排他は join・synchronized・java.util.concurrent のクラスで組み立て、優先度は既定の 5 のままにしておくのが分かりやすい方法です。

スレッドの状態

getState() が返す値は6種類です。

状態 意味 この状態になるきっかけ
NEW 作られたが、まだ開始していない new Thread(...) の直後
RUNNABLE 実行中、または実行の順番を待っている start() の後
BLOCKED synchronized のロックが空くのを待っている ほかのスレッドがロックを持っている
WAITING 期限なしで待っている join()、wait()
TIMED_WAITING 期限つきで待っている sleep(ms)、join(ms)
TERMINATED 終了した run() が終わった、または例外で抜けた
stateDiagram-v2
    [*] --> NEW: new Thread()
    NEW --> RUNNABLE: start()
    RUNNABLE --> TIMED_WAITING: sleep(ms) / join(ms)
    RUNNABLE --> WAITING: join() / wait()
    RUNNABLE --> BLOCKED: synchronized のロック待ち
    TIMED_WAITING --> RUNNABLE: 時間経過 / interrupt()
    WAITING --> RUNNABLE: 相手の終了 / notify() / interrupt()
    BLOCKED --> RUNNABLE: ロック取得
    RUNNABLE --> TERMINATED: run() の終了
    TERMINATED --> [*]

スレッドの中で例外が出たときの動き

スレッドの中で投げられた例外は、start() を呼んだ側の try-catch では受け取れません。

public class Uncaught {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { throw new IllegalStateException("在庫データが見つかりません"); }, "stock-check");
        t1.start();
        t1.join();
        System.out.println("[main] t1 で例外が出ても main は続く");

        Thread t2 = new Thread(() -> { throw new IllegalStateException("在庫データが見つかりません"); }, "stock-check-2");
        t2.setUncaughtExceptionHandler((th, ex) ->
            System.out.println("[handler] " + th.getName() + " で " + ex.getClass().getSimpleName() + ": " + ex.getMessage()));
        t2.start();
        t2.join();
        System.out.println("[main] 終了");
    }
}

実行結果(Java 21.0.10):

Exception in thread "stock-check" java.lang.IllegalStateException: 在庫データが見つかりません
    at Uncaught.lambda$main$0(Uncaught.java:3)
    at java.base/java.lang.Thread.run(Thread.java:1583)
[main] t1 で例外が出ても main は続く
[handler] stock-check-2 で IllegalStateException: 在庫データが見つかりません
[main] 終了
  • 例外が出たスレッドだけが終了し、main はそのまま続きます。 何もしなければ、標準エラーにスタックトレースが出ます(ここでもスレッド名が役に立ちます)。
  • setUncaughtExceptionHandler を設定すると、例外をログに記録するなど、自分の処理に置き換えられます。
  • 例外や結果を呼び出し側で受け取りたいときは、ExecutorService の Future.get() が向いています(ExecutionException に包まれて届きます)。

Java 25での違い(公式で確認・未実行)

この記事のコードは Java 21.0.10 で実行しています。Java 25(2025年9月リリース)について、公式の API ドキュメントと JDK 25 の JEP 一覧で確認した内容は次のとおりです。

項目 Java 21 Java 25(公式で確認)
Thread・Runnable・start・join・sleep・interrupt この記事のとおり 同じ書き方で使える
sleep(Duration)・join(Duration)・threadId() 使える(Java 19 で追加) 使える
仮想スレッド(Thread.ofVirtual() など) 正式機能 正式機能
Thread.stop() 呼ぶと UnsupportedOperationException 削除予定の非推奨のまま
suspend()・resume() 非推奨(削除予定) API ドキュメントに載っていない
Scoped Values(JEP 506) プレビュー 正式機能。ThreadLocal の代わりにスレッドへ値を渡す方法
Structured Concurrency(JEP 505) プレビュー 5回目のプレビュー(正式機能ではない)

基本の作り方と sleep・join・interrupt の扱いは、Java 21 と 25 で変わりません。

スレッドを書くときのチェックリスト

チェック項目 確認方法
run() ではなく start() を呼んでいるか Thread.currentThread().getName() を出力して確かめる
結果を使う前に join() で待っているか 「全部 start → 全部 join」の順になっているか
InterruptedException の catch が空になっていないか throws で伝えるか、Thread.currentThread().interrupt() を呼ぶ
スレッドに名前を付けたか ログ・スレッドダンプで見分けられるか
最後までやり遂げたい処理をデーモンにしていないか isDaemon() を確認
複数のスレッドが同じ変数を書き換えていないか 書き換えるなら synchronized や Atomic 系で保護する
タスクが複数あるなら ExecutorService を検討したか スレッドの使い回しと、戻り値・例外の受け取りができる

よくある質問

Q. ThreadとRunnableはどちらを使えばよいですか? A. 処理は Runnable(またはラムダ式)として書き、Thread に渡す形がおすすめです。Thread を継承するとほかのクラスを継承できなくなりますが、Runnable なら同じ処理を Thread・ExecutorService・仮想スレッドのどれにも渡せます。

Q. sleepとwaitの違いは何ですか? A. Thread.sleep() は指定時間だけ自分のスレッドを止め、synchronized のロックは持ったままです。wait() は synchronized の中で呼び、ロックを手放して notify() を待ちます。「一定時間待つ」なら sleep、「ほかのスレッドからの合図を待つ」なら wait や java.util.concurrent のクラスを使います。

Q. スレッドを途中で止めるにはどうすればよいですか? A. 止めたい相手に interrupt() を呼び、相手側は InterruptedException の catch、または isInterrupted() の確認で処理を終えます。ExecutorService を使っている場合は future.cancel(true) や shutdownNow() が同じ合図を送ります。

Q. main が終わったのにプログラムが終了しないのはなぜですか? A. デーモンでないスレッドが残っていると、JVM はその終了を待ちます。スレッドプール(Executors.newFixedThreadPool など)を閉じていない場合によく起こるので、try-with-resources で囲むか shutdown() を呼びます。裏方の処理であれば setDaemon(true) を検討します。

まとめ

  • スレッドは new Thread(() -> 処理).start() で作れる。 処理は Runnable(ラムダ式)として書くと、どの方法にも渡せる
  • start() は新しいスレッドを作り、run() はただのメソッド呼び出し。 実行中のスレッド名を出力すると違いが見える
  • join() を呼ぶと、相手のスレッドが終わるまで待つ。 待たずに結果を使うと、合計が 0 になるような結果になる
  • interrupt() は「止めてほしい」という合図。 sleep 中なら InterruptedException になり、その時点でフラグはクリアされる
  • InterruptedException の catch は空にせず、throws で伝えるか Thread.currentThread().interrupt() でフラグを戻す
  • デーモンスレッドは main の終了とともに終わる。 setDaemon(true) は start() の前に呼ぶ
  • タスクが複数あるなら ExecutorService、待ち時間の多い大量の処理なら仮想スレッドが向いている

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaスレッドの作り方|Thread・Runnable・sleep・join」

株式会社RJCでは、Javaのスレッドと並行処理の基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?