- JAVA
Javaスレッドの作り方|Thread・Runnable・sleep・join
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期連続の黒字決算。