Javaにおけるスレッドの基本
オペレーティングシステムの基本的な構成要素であるプロセスは、プログラムの実行単位であり、リソース割り当てとスケジューリングの独立した単位です。一方、スレッドはプロセス内で並行して動作するサブタスクとして理解できます。これにより、単一のプログラム内で複数の操作を同時に実行することが可能になります。
Threadクラスによるスレッド作成
Javaで新しいスレッドを作成する最も直接的な方法は、java.lang.Threadクラスを継承することです。スレッドのメインロジックは、オーバーライドされたrun()メソッド内に記述します。
public class CustomThread extends Thread {
@Override
public void run() {
System.out.println("CustomThreadが実行中...");
}
public static void main(String[] args) {
CustomThread myThreadInstance = new CustomThread();
myThreadInstance.start(); // スレッドを開始
System.out.println("メインスレッドの処理を継続...");
}
}
上記のコードを実行すると、出力は次のようになることがあります。
メインスレッドの処理を継続...
CustomThreadが実行中...
この結果からわかるように、マルチスレッド環境では、コードの呼び出し順序が必ずしも実行順序と一致しません。これはスレッドが非同期で動作するためです。
スレッド実行の非決定性
スレッドのスケジューリングはオペレーティングシステムに依存するため、実行順序は予測できません。以下の例は、メインスレッドとカスタムスレッドが不規則に実行される様子を示しています。
import java.util.Random;
public class AsyncExecutionDemo extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) { // ループ回数を調整
int delayMillis = new Random().nextInt(500); // 待機時間を短縮
Thread.sleep(delayMillis);
System.out.println("ワーカー実行中: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
System.out.println("ワーカーが中断されました。");
}
}
public static void main(String[] args) {
try {
AsyncExecutionDemo worker = new AsyncExecutionDemo();
worker.setName("BackgroundWorker"); // スレッド名を変更
worker.start();
for (int i = 0; i < 5; i++) { // ループ回数を調整
int delayMillis = new Random().nextInt(500); // 待機時間を短縮
Thread.sleep(delayMillis);
System.out.println("メイン実行中: " + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
実行結果の一例は次のとおりです。BackgroundWorkerとmainスレッドの出力が混在していることがわかります。
メイン実行中: main
ワーカー実行中: BackgroundWorker
メイン実行中: main
ワーカー実行中: BackgroundWorker
ワーカー実行中: BackgroundWorker
メイン実行中: main
ワーカー実行中: BackgroundWorker
メイン実行中: main
ワーカー実行中: BackgroundWorker
メイン実行中: main
Thread.start()メソッドは、Java仮想マシンに対し、このスレッドが実行可能状態になったことを通知し、適切なタイミングでスレッドのrun()メソッドを呼び出すようシステムに要求します。これにより非同期実行が実現されます。対照的に、thread.run()を直接呼び出すと、それは単なる通常のメソッド呼び出しとして扱われ、現在の(通常はメイン)スレッドで同期的に実行されます。つまり、run()メソッドが完了するまで次のコードは実行されません。
注意: 複数のスレッドをstart()する順序と、それらのスレッドが実際に実行を開始する順序は一致しません。
public class OrderedStartDemo extends Thread {
private int threadId;
public OrderedStartDemo(int id) {
this.threadId = id;
}
@Override
public void run() {
System.out.println("スレッドID: " + threadId);
}
public static void main(String[] args) {
OrderedStartDemo[] threads = new OrderedStartDemo[5];
for (int i = 0; i < 5; i++) {
threads[i] = new OrderedStartDemo(i + 1);
}
// 順序通りにstart()を呼び出す
for (OrderedStartDemo thread : threads) {
thread.start();
}
}
}
上記のコードの出力は、次のようにスレッドIDが不規則に表示されることがあります。
スレッドID: 2
スレッドID: 1
スレッドID: 5
スレッドID: 3
スレッドID: 4
Runnableインターフェースによるスレッド作成
Javaは多重継承をサポートしていないため、既に別のクラスを継承している場合にThreadクラスを継承することはできません。このような状況では、Runnableインターフェースを実装する方法が有用です。Runnableインターフェースはrun()メソッドのみを定義しており、実装クラスはThreadクラスのコンストラクタに渡すことでスレッドとして実行できます。
public class ExecutableTask implements Runnable {
@Override
public void run() {
System.out.println("ExecutableTaskが動作しています...");
}
public static void main(String[] args) {
Runnable task = new ExecutableTask();
Thread workerThread = new Thread(task); // Runnable実装をThreadコンストラクタに渡す
workerThread.start();
System.out.println("メインスレッド処理完了。");
}
}
実行結果の一例:
メインスレッド処理完了。
ExecutableTaskが動作しています...
Thread(Runnable target)コンストラクタは、Runnableオブジェクトだけでなく、実際にはThreadクラスのインスタンスも受け入れることができます。これは、あるThreadオブジェクトのrun()メソッドを別のスレッドに実行させる場合に利用できます。
インスタンス変数とスレッドセーフティ
データ非共有のケース
各スレッドが独自のインスタンス変数を持つ場合、データはスレッド間で共有されないため、スレッドセーフティの問題は発生しません。以下の例では、各IndividualCounterインスタンスが独自のtaskCount変数を持っています。
public class IndividualCounter extends Thread {
private int taskCount = 5;
public IndividualCounter(String threadName) {
super(threadName); // スレッド名を設定
}
@Override
public void run() {
while (taskCount > 0) {
taskCount--;
System.out.println(Thread.currentThread().getName() + " が処理中, 残り: " + taskCount);
}
}
public static void main(String[] args) {
IndividualCounter tA = new IndividualCounter("Worker-A");
IndividualCounter tB = new IndividualCounter("Worker-B");
IndividualCounter tC = new IndividualCounter("Worker-C");
tA.start();
tB.start();
tC.start();
}
}
実行結果の一例:
Worker-A が処理中, 残り: 4
Worker-B が処理中, 残り: 4
Worker-C が処理中, 残り: 4
Worker-A が処理中, 残り: 3
Worker-B が処理中, 残り: 3
Worker-C が処理中, 残り: 3
Worker-A が処理中, 残り: 2
Worker-B が処理中, 残り: 2
Worker-C が処理中, 残り: 2
Worker-A が処理中, 残り: 1
Worker-B が処理中, 残り: 1
Worker-C が処理中, 残り: 1
Worker-A が処理中, 残り: 0
Worker-B が処理中, 残り: 0
Worker-C が処理中, 残り: 0
各スレッドが独立してtaskCountを減らしているため、それぞれのスレッドが5から0までカウントダウンを完了します。
データ共有のケースとスレッドセーフティ問題
複数のスレッドが同じインスタンス変数(共有データ)にアクセスして変更する場合、スレッドセーフティの問題が発生する可能性があります。この場合、競合状態(Race Condition)が生じ、予測不能な結果やデータの一貫性のない状態を引き起こすことがあります。
public class SharedCounterTask implements Runnable {
private int sharedValue = 5; // 共有されるカウンタ
@Override
public void run() {
// ここでdecrementingを実装すると競合状態が発生する可能性あり
// 簡易的に一度の減算と出力に留める
sharedValue--; // 複数のスレッドが同時にアクセスすると問題が発生しうる
System.out.println(Thread.currentThread().getName() + " が処理中, 現在値: " + sharedValue);
}
public static void main(String[] args) {
SharedCounterTask commonTask = new SharedCounterTask(); // 一つのRunnableインスタンスを共有
Thread t1 = new Thread(commonTask, "Agent-1");
Thread t2 = new Thread(commonTask, "Agent-2");
Thread t3 = new Thread(commonTask, "Agent-3");
Thread t4 = new Thread(commonTask, "Agent-4");
Thread t5 = new Thread(commonTask, "Agent-5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
ある実行結果の例:
Agent-1 が処理中, 現在値: 3
Agent-3 が処理中, 現在値: 4
Agent-2 が処理中, 現在値: 2
Agent-5 が処理中, 現在値: 1
Agent-4 が処理中, 現在値: 0
結果から、初期値が5であったにもかかわらず、最終的なsharedValueは0になっていますが、途中の出力値が重複したり、順序が不規則であったりします。これは、sharedValue--のような単純な操作が、実際には「現在の値を取得」「値を1減らす」「新しい値を書き込む」という複数のステップで構成されており、これらのステップがアトミックではないためです。複数のスレッドが同時にこれらのステップを実行すると、不正なデータが生じる可能性があります。このような問題を解決するには、synchronizedキーワードなどを用いて、共有リソースへのアクセスを同期化する必要があります。
i--操作とSystem.out.println()におけるスレッドセーフティ
System.out.println()メソッド自体は内部で同期化されており、出力操作はアトミックに行われます。しかし、println()の引数として渡される式(例えばi--)の評価は、println()メソッドの同期ブロックに入る前に行われるため、ここでも競合状態が発生する可能性があります。
public class DecrementPrinter implements Runnable {
private int value = 5; // 共有される値
@Override
public void run() {
// value-- の評価はprintlnの同期ブロックに入る前に行われる
System.out.println("値=" + (value--) + ", スレッド名=" + Thread.currentThread().getName());
}
public static void main(String[] args) {
DecrementPrinter printerTask = new DecrementPrinter(); // 単一のRunnableインスタンス
new Thread(printerTask, "T1").start();
new Thread(printerTask, "T2").start();
new Thread(printerTask, "T3").start();
new Thread(printerTask, "T4").start();
new Thread(printerTask, "T5").start();
}
}
ある実行結果の例:
値=5, スレッド名=T1
値=4, スレッド名=T2
値=5, スレッド名=T3
値=3, スレッド名=T4
値=2, スレッド名=T5
出力からわかるように、T1とT3が同じ値5を出力しているなど、value--操作が同期化されていないために予期せぬ結果が生じています。これは、複数のスレッドが同時にvalueの元の値を取得し、それぞれが独立して減算を試みた結果です。
スレッドの状態と情報取得
Thread.currentThread()メソッド
Thread.currentThread()メソッドは、現在実行中のスレッドの参照を返します。これにより、どのスレッドが特定のコードブロックを実行しているかを識別できます。
public class ThreadInfoLogger extends Thread {
public ThreadInfoLogger() {
System.out.println("コンストラクタ開始...");
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); // メインスレッド名
System.out.println("this.getName() = " + this.getName()); // 新しいスレッドのデフォルト名 (まだ開始されていない)
System.out.println("コンストラクタ終了...");
}
@Override
public void run() {
System.out.println("runメソッド開始...");
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); // スレッドの実際の名前
System.out.println("this.getName() = " + this.getName()); // スレッドの実際の名前
System.out.println("runメソッド終了...");
}
public static void main(String[] args) {
ThreadInfoLogger logger = new ThreadInfoLogger();
Thread customThread = new Thread(logger);
customThread.setName("CustomLoggerThread"); // 明示的にスレッド名をセット
customThread.start();
}
}
実行結果:
コンストラクタ開始...
Thread.currentThread().getName() = main
this.getName() = Thread-0
コンストラクタ終了...
runメソッド開始...
Thread.currentThread().getName() = CustomLoggerThread
this.getName() = Thread-0
runメソッド終了...
コンストラクタがメインスレッドによって呼び出されるため、Thread.currentThread().getName()はmainを返します。しかし、customThread.start()が呼び出されてからrun()メソッドが実行されるとき、Thread.currentThread().getName()は設定されたスレッド名(CustomLoggerThread)を返します。なお、this.getName()はThreadインスタンス自身の名前を返すため、コンストラクタが呼び出された時点ではまだ名前が設定されていない場合、デフォルト名(Thread-0など)を返します。
isAlive()メソッド
isAlive()メソッドは、スレッドが「生きている」状態(つまり、開始され、まだ終了していない状態)にあるかどうかを判定します。スレッドが実行中であるか、実行の準備ができている状態であれば、trueを返します。
public class LivenessChecker extends Thread {
@Override
public void run() {
System.out.println("runメソッド内: isAlive()=" + this.isAlive());
}
public static void main(String[] args) {
LivenessChecker checkerThread = new LivenessChecker();
System.out.println("開始前: isAlive()=" + checkerThread.isAlive()); // false
checkerThread.start();
System.out.println("start呼び出し後: isAlive()=" + checkerThread.isAlive()); // true (通常)
}
}
実行結果:
開始前: isAlive()=false
start呼び出し後: isAlive()=true
runメソッド内: isAlive()=true
start()呼び出し直後にisAlive()がtrueになるのは、スレッドが実行中または実行待ちの状態にあるためです。しかし、スレッドのrun()メソッドが非常に短時間で完了する場合、メインスレッドがisAlive()をチェックする頃には、スレッドは既に終了している可能性があります。次の例はそれを考慮しています。
public class QuickLivenessChecker extends Thread {
@Override
public void run() {
// 短い処理
System.out.println("クイックタスク実行中: isAlive()=" + this.isAlive());
}
public static void main(String[] args) throws InterruptedException {
QuickLivenessChecker shortTaskThread = new QuickLivenessChecker();
System.out.println("開始前: isAlive()=" + shortTaskThread.isAlive()); // false
shortTaskThread.start();
Thread.sleep(100); // 少し待機して、短いタスクが完了するのを「期待」する
System.out.println("タスク完了後 (推定): isAlive()=" + shortTaskThread.isAlive()); // false になる可能性が高い
}
}
実行結果の一例:
開始前: isAlive()=false
クイックタスク実行中: isAlive()=true
タスク完了後 (推定): isAlive()=false
Thread.sleep(100)によりメインスレッドが一時停止している間にshortTaskThreadが完了したため、その後のisAlive()呼び出しでfalseが返されています。
sleep()メソッド
Thread.sleep(long millis)メソッドは、現在実行中のスレッド(Thread.currentThread()が返すスレッド)を指定されたミリ秒数だけ休止させます。これにより、スレッドの実行が一時的に中断されます。
public class SleepDemoTask extends Thread {
@Override
public void run() {
try {
System.out.println("スリープタスク開始: " + Thread.currentThread().getName());
Thread.sleep(1500); // 1.5秒休止
System.out.println("スリープタスク終了: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println("スリープ中に中断されました。");
}
}
public static void main(String[] args) {
SleepDemoTask syncDemo = new SleepDemoTask();
System.out.println("メイン処理開始時刻: " + System.currentTimeMillis());
syncDemo.run(); // start()ではなくrun()を直接呼び出すため、同期的に実行される
System.out.println("メイン処理終了時刻: " + System.currentTimeMillis());
}
}
実行結果:
メイン処理開始時刻: 1678886400000 (例)
スリープタスク開始: main
スリープタスク終了: main
メイン処理終了時刻: 1678886401501 (例)
この例ではstart()ではなくrun()が直接呼び出されているため、SleepDemoTaskのrun()メソッドはメインスレッドで実行され、全体の処理時間がスリープ時間分だけ長くなっています。
getId()メソッド
getId()メソッドは、スレッドを一意に識別するためのロング型のIDを返します。このIDはJava仮想マシン内でスレッドごとに一意であることが保証されます。
public class ThreadIdentifier {
public static void main(String[] args) {
Thread current = Thread.currentThread();
System.out.println("現在のスレッド名: " + current.getName() + ", ID: " + current.getId());
Thread newThread = new Thread(() -> System.out.println("新規スレッド名: " + Thread.currentThread().getName() + ", ID: " + Thread.currentThread().getId()));
newThread.start();
}
}
実行結果の一例:
現在のスレッド名: main, ID: 1
新規スレッド名: Thread-0, ID: 12 (例: 環境により異なる)
スレッドの停止
スレッドを安全に停止させることは、マルチスレッドプログラミングにおける重要な課題です。Thread.stop()メソッドはスレッドを強制的に停止させますが、これは非推奨(deprecated)であり、データの一貫性を損なう可能性があるため使用すべきではありません。Javaでは、通常、以下の3つのアプローチでスレッドを終了させます。
- 終了フラグの使用:
run()メソッドが正常に完了するように、ループ条件などでフラグをチェックして終了を促す。 stop()メソッドの使用: 非推奨であり、データ破損のリスクがあるため避けるべき。interrupt()メソッドの使用: スレッドに中断を要求し、スレッド自身が中断フラグをチェックして終了処理を行う。
interrupt()を呼び出しても停止しないスレッドの例
Thread.interrupt()メソッドは、スレッドに「中断された」というシグナルを送るだけで、即座にスレッドを停止させるわけではありません。スレッド自身がこの中断シグナルを検知し、適切に処理を行う必要があります。
public class UnstoppableTask extends Thread {
@Override
public void run() {
System.out.println("タスク開始 (中断されません)...");
for (int i = 0; i < 200000; i++) { // ループ回数を調整
// 中断フラグをチェックしないため、割り込みシグナルを無視する
System.out.println("処理中: " + i);
}
System.out.println("タスク完了。");
}
public static void main(String[] args) {
try {
UnstoppableTask task = new UnstoppableTask();
task.start();
Thread.sleep(50); // メインスレッドで少し待機
task.interrupt(); // タスクに中断シグナルを送る
System.out.println("メインスレッドが中断シグナルを送信しました。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上記コードの出力を見ると、interrupt()が呼び出された後も、ループは最後まで実行され続けることがわかります。これは、run()メソッド内で中断フラグをチェックしていないためです。
スレッドの中断状態の確認
Threadクラスには、スレッドの中断状態を確認するための2つのメソッドがあります。
Thread.interrupted(): 現在のスレッド(Thread.currentThread())が中断されているかどうかをテストし、かつ、そのスレッドの中断状態をクリア(falseにリセット)します。Thread.isInterrupted(): スレッドが中断されているかどうかをテストしますが、中断状態をクリアしません。
public class InterruptStatusChecker {
public static void main(String[] args) {
Thread.currentThread().interrupt(); // メインスレッドを中断状態にする
System.out.println("メインスレッドが中断されているか?(interrupted()): " + Thread.interrupted()); // true (同時にフラグがクリアされる)
System.out.println("再度確認 (interrupted()): " + Thread.interrupted()); // false (フラグはクリア済み)
}
}
public class ThreadInterruptCheck extends Thread {
@Override
public void run() {
System.out.println("タスク実行中...");
// isInterrupted() はフラグをクリアしない
System.out.println("runメソッド内: " + Thread.currentThread().getName() + " が中断されているか?: " + Thread.currentThread().isInterrupted());
}
public static void main(String[] args) {
try {
ThreadInterruptCheck targetThread = new ThreadInterruptCheck();
targetThread.start();
// メインスレッドから対象スレッドの中断状態をチェック
System.out.println("start直後: " + targetThread.getName() + " が中断されているか?: " + targetThread.isInterrupted());
targetThread.interrupt(); // 対象スレッドに中断シグナルを送る
System.out.println("interrupt()呼び出し後: " + targetThread.getName() + " が中断されているか?: " + targetThread.isInterrupted());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("メインスレッドの処理を継続...");
}
}
実行結果の一例:
start直後: Thread-0 が中断されているか?: false
タスク実行中...
runメソッド内: Thread-0 が中断されているか?: false
interrupt()呼び出し後: Thread-0 が中断されているか?: true
メインスレッドの処理を継続...
run()メソッドが実行を完了した後にinterrupt()が呼び出された場合、run()メソッド内では中断状態はfalseとして検出され、メインスレッドからinterrupt()呼び出し後にisInterrupted()をチェックするとtrueが返されます。スレッドが既に終了している場合でも、その中断状態は保持されます。
中断を伴う安全なスレッド停止 (例外利用)
interrupt()メソッドを使用してスレッドを安全に停止させるには、スレッドのrun()メソッド内で定期的に中断状態をチェックし、trueであれば処理を中止する必要があります。ループ処理中に中断が要求された場合、breakを使用してループを終了できます。
public class InterruptibleLoopTask extends Thread {
@Override
public void run() {
System.out.println("ループタスク開始...");
for (int i = 0; i < 50000; i++) {
if (Thread.currentThread().isInterrupted()) { // isInterrupted()を使用
System.out.println("中断が要求されました。ループを終了します。");
break; // ループを抜ける
}
System.out.println("現在の値: " + i);
}
System.out.println("ループ後の処理を継続..."); // ループを抜けてもここが実行される
}
public static void main(String[] args) {
try {
InterruptibleLoopTask task = new InterruptibleLoopTask();
task.start();
Thread.sleep(100); // 少し待機
task.interrupt(); // 中断シグナルを送る
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("メインスレッド処理完了。");
}
}
実行結果の一例:
...
現在の値: 41710
現在の値: 41711
現在の値: 41712
中断が要求されました。ループを終了します。
ループ後の処理を継続...
メインスレッド処理完了。
このアプローチでは、ループを抜けた後もrun()メソッド内の残りのコードが実行されます。もし、中断が要求されたらスレッド全体の実行を即座に停止したい場合は、InterruptedExceptionをスローする方法が有効です。
public class ExceptionOnInterruptTask extends Thread {
@Override
public void run() {
try {
System.out.println("例外処理を伴うループタスク開始...");
for (int i = 0; i < 50000; i++) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("中断が要求されました。例外をスローします。");
throw new InterruptedException("タスクが中断されました。"); // 例外をスロー
}
System.out.println("現在の処理ステップ: " + i);
}
System.out.println("この行は実行されません (例外がスローされた場合)。");
} catch (InterruptedException e) {
System.out.println("InterruptedExceptionをキャッチしました: " + e.getMessage());
// 例外をログに記録するか、さらに上位に伝播させる
} finally {
System.out.println("finallyブロック実行。リソースをクリーンアップします。");
}
}
public static void main(String[] args) {
try {
ExceptionOnInterruptTask task = new ExceptionOnInterruptTask();
task.start();
Thread.sleep(100);
task.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("メインスレッドの処理完了。");
}
}
実行結果の一例:
...
現在の処理ステップ: 42823
現在の処理ステップ: 42824
中断が要求されました。例外をスローします。
InterruptedExceptionをキャッチしました: タスクが中断されました。
finallyブロック実行。リソースをクリーンアップします。
メインスレッドの処理完了。
この方法では、InterruptedExceptionをキャッチすることで、中断シグナルがスレッドの実行を効果的に停止させ、クリーンアップ処理(finallyブロックなど)を実行する機会を与えます。
スリープ中のスレッド停止
スレッドがsleep()やwait()、join()などのブロッキングメソッドを呼び出している最中にinterrupt()されると、これらのメソッドはInterruptedExceptionをスローし、同時にスレッドの中断状態フラグがクリアされます(falseになる)。
public class SleepingTask extends Thread {
@Override
public void run() {
try {
System.out.println("スリープ開始...");
Thread.sleep(20000); // 長いスリープ
System.out.println("スリープ終了...");
} catch (InterruptedException e) {
System.out.println("スリープ中に中断されました。中断状態: " + this.isInterrupted()); // false になる
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
SleepingTask task = new SleepingTask();
task.start();
Thread.sleep(200); // 短く待機
task.interrupt(); // スリープ中のタスクを中断
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("メインスレッド処理完了。");
}
}
実行結果:
スリープ開始...
メインスレッド処理完了。
スリープ中に中断されました。中断状態: false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at SleepingTask.run(SleepingTask.java:8)
InterruptedExceptionがスローされた際、スレッドの中断状態が自動的にクリアされる(falseになる)点に注意が必要です。
もしスレッドが大量の計算処理を行った後にスリープ状態に入る場合、その挙動は以下のようになります。
public class MixedTask extends Thread {
@Override
public void run() {
try {
System.out.println("計算ループ開始");
for (int i = 0; i < 100000; i++) {
// ここで中断フラグをチェックしない限り、ループは継続
// たとえinterrupt()が呼び出されても
if (Thread.currentThread().isInterrupted()) { // ループ中にチェックを追加
System.out.println("ループ中に中断を検知!");
throw new InterruptedException("ループ処理が中断されました。");
}
System.out.println("計算中: " + i);
}
System.out.println("計算ループ終了");
System.out.println("スリープ開始...");
Thread.sleep(20000); // 長いスリープ
System.out.println("スリープ終了...");
} catch (InterruptedException e) {
System.out.println("MixedTaskが中断されました。中断状態: " + this.isInterrupted());
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("メインスレッド開始...");
try {
MixedTask task = new MixedTask();
task.start();
Thread.sleep(200); // 計算中に中断シグナルを送る
task.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("メインスレッド終了...");
}
}
実行結果の一例:
計算ループ開始
...
計算中: 16543
計算中: 16544
計算中: 16545
ループ中に中断を検知!
MixedTaskが中断されました。中断状態: false
java.lang.InterruptedException: ループ処理が中断されました。
at MixedTask.run(MixedTask.java:12)
メインスレッド終了...
この場合、計算ループ中にinterrupt()が呼び出されても、isInterrupted()がチェックされるまでループは継続します。チェック後にInterruptedExceptionがスローされ、finallyブロックやキャッチブロックに処理が移ります。
stop()メソッドによる強制停止 (非推奨)
Thread.stop()メソッドは、スレッドの実行を即座に強制終了させます。しかし、この方法はデータの一貫性を損なう可能性が非常に高いため、使用は強く非推奨とされています。
public class ForceStopDemo extends Thread {
@Override
public void run() {
System.out.println("強制停止デモ開始");
for (long i = 0; i < 1_000_000_000L; i++) { // 長いループ
if (i % 100_000_000 == 0) { // 適度にプログレスを出力
System.out.println("処理中: " + i);
}
}
System.out.println("強制停止デモ終了 (この行は表示されないはず)");
}
public static void main(String[] args) {
System.out.println("メインスレッド開始...");
try {
ForceStopDemo task = new ForceStopDemo();
task.start();
Thread.sleep(10); // 短時間待機
task.stop(); // スレッドを強制停止
System.out.println("スレッドを強制停止しました。");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("メインスレッド終了...");
}
}
実行結果の一例:
メインスレッド開始...
強制停止デモ開始
処理中: 0
スレッドを強制停止しました。
メインスレッド終了...
stop()が呼び出されると、スレッドは突然停止し、クリーンアップやリソース解放の機会が失われます。run()メソッド内の「強制停止デモ終了」という出力は表示されません。
stop()メソッドとjava.lang.ThreadDeath例外
stop()メソッドが呼び出されると、内部的にThreadDeathという特殊な例外がスローされます。通常、この例外を明示的にキャッチする必要はありませんが、キャッチすることでその挙動を確認できます。
public class ThreadDeathCatcher extends Thread {
@Override
public void run() {
try {
System.out.println("ThreadDeathCatcherスレッド開始。");
// 自分自身を停止させることでThreadDeathを発生させる
Thread.currentThread().stop();
System.out.println("この行は実行されません。");
} catch (ThreadDeath e) {
System.out.println("ThreadDeath例外を捕捉しました。");
e.printStackTrace();
} finally {
System.out.println("finallyブロックでクリーンアップ作業。");
}
}
public static void main(String[] args) {
ThreadDeathCatcher catcher = new ThreadDeathCatcher();
catcher.start();
}
}
実行結果:
ThreadDeathCatcherスレッド開始。
ThreadDeath例外を捕捉しました。
java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:827)
at ThreadDeathCatcher.run(ThreadDeathCatcher.java:8)
finallyブロックでクリーンアップ作業。
ThreadDeath例外は、スレッドの強制終了メカニズムの一部として使用されます。
stop()メソッドによるロック解放の悪影響
stop()メソッドの最も危険な側面の一つは、スレッドが取得していたモニターロック(synchronizedブロックなど)を予期せず解放してしまうことです。これにより、共有データが不完全な状態のまま他のスレッドに公開され、データの一貫性が損なわれる可能性があります。
public class SharedResource {
private String resourceId = "DEFAULT";
private int resourceValue = 0;
public String getResourceId() { return resourceId; }
public int getResourceValue() { return resourceValue; }
// 同期メソッドでリソースを更新
public synchronized void updateResource(String id, int value) {
try {
this.resourceId = id; // 最初のフィールドを更新
Thread.sleep(2000); // 長い処理をシミュレート
this.resourceValue = value; // 二番目のフィールドを更新
System.out.println("リソース更新完了: ID=" + id + ", Value=" + value);
} catch (InterruptedException e) {
System.out.println("リソース更新中に中断されました。");
}
}
}
public class ResourceUpdater extends Thread {
private SharedResource resource;
public ResourceUpdater(SharedResource res) {
this.resource = res;
}
@Override
public void run() {
resource.updateResource("UPDATED", 999);
}
public static void main(String[] args) {
try {
SharedResource commonResource = new SharedResource();
ResourceUpdater updaterThread = new ResourceUpdater(commonResource);
updaterThread.start();
Thread.sleep(100); // アップデートが部分的に進行するのを待つ
// updaterThreadを強制停止する
updaterThread.stop(); // ロックを強制的に解放し、一貫性のない状態に
System.out.println("スレッドをstop()で強制停止しました。");
// 共有リソースの状態を確認
System.out.println("現在のリソース状態: ID=" + commonResource.getResourceId() + ", Value=" + commonResource.getResourceValue());
} catch (Exception e) {
e.printStackTrace();
}
}
}
実行結果:
スレッドをstop()で強制停止しました。
現在のリソース状態: ID=UPDATED, Value=0
ResourceUpdaterスレッドはresourceIdを"UPDATED"に更新しましたが、Thread.sleep()中にstop()が呼び出されたため、resourceValueが更新される前に強制終了されてしまいました。結果として、SharedResourceはID="UPDATED"とValue=0という矛盾した状態になり、データの一貫性が失われています。このため、stop()メソッドはJavaのマルチスレッドプログラミングでは絶対に使用すべきではありません。
returnによる安全なスレッド停止
interrupt()メソッドとreturnステートメントを組み合わせることで、run()メソッドの実行を安全に終了させることができます。これは、run()メソッド内のループで中断状態をチェックし、trueであればreturnでメソッドから抜けるというアプローチです。
public class ReturnOnInterruptTask extends Thread {
@Override
public void run() {
System.out.println("ReturnOnInterruptTask開始。");
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("中断が検出されました。スレッドを終了します。");
return; // run()メソッドから抜けてスレッドを終了
}
System.out.println("現在時刻: " + System.currentTimeMillis());
try {
Thread.sleep(100); // 短時間スリープを挟むことで、頻繁な出力とCPU負荷を軽減
} catch (InterruptedException e) {
System.out.println("スリープ中に中断されました。スレッドを終了します。");
return; // スリープ中でも中断されたら終了
}
}
}
public static void main(String[] args) throws InterruptedException {
ReturnOnInterruptTask task = new ReturnOnInterruptTask();
task.start();
Thread.sleep(500); // 0.5秒後に中断
task.interrupt();
}
}
実行結果の一例:
ReturnOnInterruptTask開始。
現在時刻: 1678886400000
現在時刻: 1678886400100
現在時刻: 1678886400200
現在時刻: 1678886400300
現在時刻: 1678886400400
スリープ中に中断されました。スレッドを終了します。
この方法は、スレッドをクリーンに終了させるための有効な手段の一つです。ただし、前述の「例外をスローする方法」の方が、中断イベントをより構造的に処理し、コールスタックを通じて伝播させることで、複雑なスレッドロジックにおいてより柔軟な対応が可能となる場合があります。
スレッドの一時停止と再開 (非推奨)
Javaには、suspend()メソッドでスレッドを一時停止し、resume()メソッドで再開する機能がありましたが、これらはデッドロックやデータ不整合を引き起こす可能性が高いため、現在では非推奨(deprecated)となっています。これらのメソッドは、使用すべきではありません。
suspend()とresume()の基本的な動作
public class ControllableCounter extends Thread {
private volatile long counter = 0; // volatileで可視性を確保
public long getCounter() {
return counter;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "スレッド開始...");
while (true) {
counter++;
// 極力CPUを消費しないように短いsleepを入れることも考慮
// try { Thread.sleep(1); } catch (InterruptedException e) { break; }
}
}
public static void main(String[] args) {
try {
ControllableCounter myCounterThread = new ControllableCounter();
myCounterThread.setName("CountingThread");
myCounterThread.start();
Thread.sleep(500);
System.out.println("メインスレッドからの経過観察 (開始) time=" + System.currentTimeMillis() + ", counter=" + myCounterThread.getCounter());
myCounterThread.suspend(); // スレッドを一時停止
Thread.sleep(500);
System.out.println("メインスレッドからの経過観察 (一時停止中) time=" + System.currentTimeMillis() + ", counter=" + myCounterThread.getCounter());
Thread.sleep(500);
System.out.println("メインスレッドからの経過観察 (一時停止中) time=" + System.currentTimeMillis() + ", counter=" + myCounterThread.getCounter());
myCounterThread.resume(); // スレッドを再開
Thread.sleep(500);
System.out.println("メインスレッドからの経過観察 (再開後) time=" + System.currentTimeMillis() + ", counter=" + myCounterThread.getCounter());
myCounterThread.suspend(); // 再度一時停止
Thread.sleep(500);
System.out.println("メインスレッドからの経過観察 (再一時停止中) time=" + System.currentTimeMillis() + ", counter=" + myCounterThread.getCounter());
myCounterThread.stop(); // 最後に停止 (非推奨だがデモのため)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
実行結果:
CountingThreadスレッド開始...
メインスレッドからの経過観察 (開始) time=1678886400500, counter=123456789 (例)
メインスレッドからの経過観察 (一時停止中) time=1678886401000, counter=543210987 (例)
メインスレッドからの経過観察 (一時停止中) time=1678886401500, counter=543210987 (例)
メインスレッドからの経過観察 (再開後) time=1678886402000, counter=876543210 (例)
メインスレッドからの経過観察 (再一時停止中) time=1678886402500, counter=987654321 (例)
suspend()とresume()を使用すると、カウンター値の増加が一時停止したり再開したりしていることがわかります。しかし、これらのメソッドは非常に危険です。
suspend()によるデッドロックの発生
suspend()メソッドの主な欠点の一つは、スレッドがモニターロックを保持したまま一時停止してしまう可能性がある点です。これにより、他のスレッドがそのロックを取得できなくなり、デッドロックに陥ることがあります。
public class SynchronizedPauseDemo extends Thread {
public synchronized void criticalSection() {
System.out.println("クリティカルセクション開始: " + Thread.currentThread().getName());
if (Thread.currentThread().getName().equals("BlockingThread")) {
System.out.println("BlockingThreadがクリティカルセクション内で永久に停止...");
Thread.currentThread().suspend(); // ここでロックを保持したまま停止
}
System.out.println("クリティカルセクション終了: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
try {
final SynchronizedPauseDemo demoInstance = new SynchronizedPauseDemo();
Thread t1 = new Thread(() -> demoInstance.criticalSection());
t1.setName("BlockingThread");
t1.start();
Thread.sleep(500); // t1がクリティカルセクションに入るのを待つ
Thread t2 = new Thread(() -> {
System.out.println("SecondThreadが起動しましたが、クリティカルセクションに入れません...");
demoInstance.criticalSection(); // t1がロックを保持しているためブロックされる
});
t2.setName("SecondThread");
t2.start();
// t1.resume(); // この行をコメントアウトするとt2は永遠にブロックされる
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
実行結果:
クリティカルセクション開始: BlockingThread
BlockingThreadがクリティカルセクション内で永久に停止...
SecondThreadが起動しましたが、クリティカルセクションに入れません...
BlockingThreadがcriticalSection()に入り、そこでsuspend()が呼び出されます。このとき、BlockingThreadはdemoInstanceオブジェクトのロックを保持したまま一時停止します。次にSecondThreadがcriticalSection()を呼び出そうとしますが、ロックが解放されていないため永久に待機状態となり、プログラムは停止します。これがsuspend()とresume()が非推奨である主要な理由です。
また、別のデッドロックシナリオとして、System.out.println()が内部的に同期されていることが挙げられます。println()が実行中にスレッドがsuspend()されると、println()が持つ内部ロックが解放されず、他のスレッドからのprintln()呼び出しもブロックされる可能性があります。
public class PrintBlockingPause extends Thread {
private long dataCount = 0;
@Override
public void run() {
System.out.println("PrintBlockingPauseスレッド開始...");
while (true) {
dataCount++;
System.out.println("カウント: " + dataCount); // printlnは同期メソッド
// ここでsuspend()されると、System.outのロックを解放しないまま停止する可能性
}
}
public static void main(String[] args) {
try {
System.out.println("メインスレッド開始...");
PrintBlockingPause worker = new PrintBlockingPause();
worker.start();
Thread.sleep(10); // workerスレッドが少し実行されるのを待つ
worker.suspend(); // workerスレッドを一時停止
System.out.println("メインスレッド終了..."); // この行がブロックされる可能性
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
実行結果の一例:
メインスレッド開始...
PrintBlockingPauseスレッド開始...
カウント: 1
カウント: 2
...
カウント: 804
カウント: 805
この後、プログラムは停止します。worker.suspend()が呼び出された時点で、System.out.println()がまだ完了していない場合、println()が保持するロックが解放されません。その結果、メインスレッドが次にSystem.out.println("メインスレッド終了...")を呼び出そうとしたときに、そのロックを取得できずにブロックされてしまいます。
suspend()とresume()のもう一つの欠点: データ不整合
suspend()メソッドは、データが部分的に更新された状態でもスレッドを停止させるため、共有データに一貫性のない状態をもたらす可能性があります。
public class DataInconsistencyDemo extends Thread {
private String userName = "initial_user";
private String userRole = "initial_role";
public void setCredentials(String name, String role) {
this.userName = name;
if (Thread.currentThread().getName().equals("SetterThread")) {
System.out.println("SetterThreadが途中で停止...");
Thread.currentThread().suspend(); // ここで停止
}
this.userRole = role; // この行は実行されない可能性
}
public void displayCredentials() {
System.out.println("資格情報: ユーザー名=" + userName + ", ロール=" + userRole);
}
public static void main(String[] args) throws InterruptedException {
final DataInconsistencyDemo demo = new DataInconsistencyDemo();
Thread t1 = new Thread(() -> demo.setCredentials("new_user", "admin"));
t1.setName("SetterThread");
t1.start();
Thread.sleep(100); // t1がuserNameを設定し、suspendするのを待つ
Thread t2 = new Thread(() -> demo.displayCredentials());
t2.start();
}
}
実行結果:
SetterThreadが途中で停止...
資格情報: ユーザー名=new_user, ロール=initial_role
SetterThreadがsetCredentialsメソッド内でuserNameを更新した後、suspend()によって一時停止されました。このとき、userRoleの更新は行われていません。その直後にt2がdisplayCredentials()を呼び出すと、部分的に更新された(一貫性のない)データが表示されてしまいます。これらの理由から、suspend()とresume()は安全ではなく、使用を避けるべきです。
yield()メソッド
Thread.yield()メソッドは、現在実行中のスレッドがCPUリソースを一時的に放棄し、他の実行可能なスレッドにCPUの実行時間を与えることを示唆します。これはスケジューラに対するヒントであり、必ずしも他のスレッドがすぐに実行されることを保証するものではなく、放棄する時間も不確定です。
public class YieldDemonstrator extends Thread {
@Override
public void run() {
long startTime = System.nanoTime(); // ナノ秒単位で時間を計測
long sum = 0;
for (int i = 0; i < 50_000_000; i++) { // ループ回数を調整
Thread.yield(); // CPUを譲る
sum += i;
}
long endTime = System.nanoTime();
System.out.println(Thread.currentThread().getName() + " (yield使用) 処理時間: " + (endTime - startTime) / 1_000_000 + "ms, 合計: " + sum);
}
public static void main(String[] args) {
YieldDemonstrator yieldThread = new YieldDemonstrator();
yieldThread.setName("YieldingThread");
yieldThread.start();
// yieldなしの比較用スレッド
new Thread(() -> {
long startTime = System.nanoTime();
long sum = 0;
for (int i = 0; i < 50_000_000; i++) {
sum += i;
}
long endTime = System.nanoTime();
System.out.println(Thread.currentThread().getName() + " (yieldなし) 処理時間: " + (endTime - startTime) / 1_000_000 + "ms, 合計: " + sum);
}, "NonYieldingThread").start();
}
}
実行結果の一例:
NonYieldingThread (yieldなし) 処理時間: 50ms, 合計: 1249999975000000
YieldingThread (yield使用) 処理時間: 500ms, 合計: 1249999975000000
yield()を使用しないスレッドと比較して、yield()を使用したスレッドの方が処理時間が長くなる傾向があります。これは、CPU時間を積極的に手放しているためです。ただし、この効果は実行環境(OS、CPU、JVMのスケジューリングポリシー)によって大きく異なる場合があります。
スレッドの優先度
オペレーティングシステムでは、スレッドに優先度を設定できます。優先度の高いスレッドは、より多くのCPUリソースを受け取る可能性が高く、CPUによって優先的にスケジューリングされます。Javaでは、スレッドの優先度は1(最低)から10(最高)までの範囲で設定され、デフォルトの優先度は5です。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
setPriority(int newPriority)メソッドを使用して優先度を設定できますが、有効な範囲外の値を設定しようとするとIllegalArgumentExceptionがスローされます。
スレッド優先度の継承性
Javaのスレッド優先度には継承性があります。あるスレッド(親スレッド)が新しいスレッド(子スレッド)を起動した場合、特別に設定しない限り、子スレッドは親スレッドと同じ優先度を継承します。
public class InheritedPriorityChild extends Thread {
@Override
public void run() {
System.out.println("子スレッドの優先度: " + this.getPriority());
}
}
public class PriorityInheritanceDemo extends Thread {
@Override
public void run() {
System.out.println("親スレッドの優先度 (子を起動する前): " + this.getPriority());
InheritedPriorityChild child = new InheritedPriorityChild();
child.start();
}
public static void main(String[] args) {
System.out.println("メインスレッドの初期優先度: " + Thread.currentThread().getPriority()); // デフォルトは5
Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1); // メインスレッドの優先度を9に設定
System.out.println("メインスレッドの変更後優先度: " + Thread.currentThread().getPriority()); // 9
PriorityInheritanceDemo parentThread = new PriorityInheritanceDemo();
parentThread.start(); // 親スレッドを起動
}
}
実行結果:
メインスレッドの初期優先度: 5
メインスレッドの変更後優先度: 9
親スレッドの優先度 (子を起動する前): 9
子スレッドの優先度: 9
この結果から、PriorityInheritanceDemoスレッド(親)はメインスレッドの優先度9を継承し、さらにInheritedPriorityChildスレッド(子)はPriorityInheritanceDemoスレッドの優先度9を継承していることがわかります。
優先度の非決定性
setPriority()メソッドでスレッドの優先度を設定することはできますが、オペレーティングシステムは可能な限り優先度の高いスレッドにリソースを割り当てようとするだけで、必ずしも高い優先度のスレッドが低い優先度のスレッドより先に、またはより長く実行されることを保証するものではありません。特に、OSのスケジューリングアルゴリズムやシステム負荷によっては、優先度の効果が限定的になることがあります。
import java.util.Random;
public class HighPriorityWorker extends Thread {
@Override
public void run() {
long startTime = System.currentTimeMillis();
long accumulator = 0;
Random rnd = new Random();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 50000; j++) {
rnd.nextInt(); // 軽い計算をシミュレート
accumulator += j;
}
}
long endTime = System.currentTimeMillis();
System.out.println("▲▲▲高優先度タスク完了▲▲▲ - 処理時間: " + (endTime - startTime) + "ms");
}
}
import java.util.Random;
public class LowPriorityWorker extends Thread {
@Override
public void run() {
long startTime = System.currentTimeMillis();
long accumulator = 0;
Random rnd = new Random();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 50000; j++) {
rnd.nextInt(); // 軽い計算をシミュレート
accumulator += j;
}
}
long endTime = System.currentTimeMillis();
System.out.println("▼▼▼低優先度タスク完了▼▼▼ - 処理時間: " + (endTime - startTime) + "ms");
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) { // 少ない回数でデモ
HighPriorityWorker highTask = new HighPriorityWorker();
highTask.setPriority(Thread.MAX_PRIORITY); // 最高優先度 (10)
LowPriorityWorker lowTask = new LowPriorityWorker();
lowTask.setPriority(Thread.MIN_PRIORITY); // 最低優先度 (1)
highTask.start();
lowTask.start();
}
}
}
実行結果の一部:
▲▲▲高優先度タスク完了▲▲▲ - 処理時間: 120ms
▼▼▼低優先度タスク完了▼▼▼ - 130ms
▲▲▲高優先度タスク完了▲▲▲ - 115ms
▲▲▲高優先度タスク完了▲▲▲ - 100ms
▼▼▼低優先度タスク完了▼▼▼ - 150ms
▲▲▲高優先度タスク完了▲▲▲ - 110ms
▼▼▼低優先度タスク完了▼▼▼ - 125ms
▼▼▼低優先度タスク完了▼▼▼ - 140ms
▲▲▲高優先度タスク完了▲▲▲ - 105ms
▼▼▼低優先度タスク完了▼▼▼ - 135ms
この結果から、高優先度スレッドが常に先に完了したり、一貫して短い処理時間を示したりするわけではないことがわかります。OSのスケジューリングは複雑であり、Javaの優先度設定はあくまでヒントとして扱われます。したがって、スレッドの順序や実行時間について厳密な保証を期待すべきではありません。
デーモンスレッド
Javaには、「ユーザー(非デーモン)スレッド」と「デーモンスレッド」の2種類のスレッドがあります。デーモンスレッドは、通常のユーザー(非デーモン)スレッドがすべて終了した時点で自動的に終了する特別な種類のスレッドです。典型的には、ガベージコレクタースレッドなどがデーモンスレッドとして動作します。プロセス内にユーザー(非デーモン)スレッドが存在しない場合、デーモンスレッドは実行を続ける必要がないため、自動的にシャットダウンされます。
setDaemon(true)メソッドを呼び出すことで、スレッドをデーモンスレッドとしてマークできます。これはスレッドが開始される前に行う必要があります。
public class DaemonLifeCycle extends Thread {
private int loopCount = 0;
@Override
public void run() {
try {
while (true) { // 無限ループ
loopCount++;
System.out.println("デーモンスレッド実行中: カウント=" + loopCount);
Thread.sleep(800); // 少し短縮
}
} catch (InterruptedException e) {
System.out.println("デーモンスレッドが中断されました。");
} finally {
System.out.println("デーモンスレッド終了処理。");
}
}
public static void main(String[] args) {
try {
DaemonLifeCycle daemonTask = new DaemonLifeCycle();
daemonTask.setDaemon(true); // デーモンスレッドに設定
daemonTask.start();
Thread.sleep(4000); // メインスレッドが4秒間だけ実行
System.out.println("メインスレッド終了。デーモンスレッドも自動終了するはずです。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
実行結果:
デーモンスレッド実行中: カウント=1
デーモンスレッド実行中: カウント=2
デーモンスレッド実行中: カウント=3
デーモンスレッド実行中: カウント=4
メインスレッド終了。デーモンスレッドも自動終了するはずです。
デーモンスレッド終了処理。
メインスレッドがThread.sleep(4000)を終えて終了すると、それ以上ユーザー(非デーモン)スレッドが存在しなくなるため、デーモンスレッドであるDaemonLifeCycleスレッドも自動的に終了します。finallyブロックが実行されていることから、スレッドは強制的に終了されるものの、最低限のクリーンアップは試みられることがわかります。