Java スレッドの状態遷移と基本制御API

Java のシングルスレッドモデルから多コア環境への対応として、非同期処理の実現にはスレッド機能が不可欠です。スレッドのライフサイクルを理解し、適切に制御することが安定したシステム構築の基盤となります。

スレッドの基本状態

JVM 上のスレッドは、主に以下の 5 つの状態を移行します。

  • 新建(NEW): スレッドオブジェクトがインスタンス化された直後の状態です。まだ実行開始のシグナル(start メソッド)を送っていないため、CPU は割り当てていません。
  • ランナブル(RUNNABLE): start() メソッドが呼び出され、OS にスケジューリングを依頼しています。CPU が割り当てられるのを待機中であり、実行可能な状態です。
  • 実行中(RUNNING): OS がこのスレッドを選択し、実際にコードが実行されている状態です。ランナブル状態からのみこの状態へ遷移可能です。
  • ブロック状態(BLOCKED): 何らかの理由で CPU の使用権を一時的に失い、実行を停止している状態です。再度スケジュールされるまで待機します。具体的には以下に分けられます:
    • 待ち(WAITING): wait() メソッドを実行した場合や他のスレッドの終了(join())を待っている場合。
    • 同期待ち(SYNC_WAIT): synchronized ブロックを取得しようとした際、競合が発生してロック待ちとなっている場合。
    • I/O または休止(I/O / TIMED_WAITING): sleep(), join(), I/O 操作などで待機している場合。
  • 終了(TERMINATED): run() メソッドが正常に完了するか、未処理の例外によりメソッドが退出した時点で生命周期は閉じます。

スレッド作成の 3 パターン

タスクを並列実行する際の主要な方式は以下の通りです。

1. Thread クラスを継承する

最も単純な方式ですが、クラス継承により他の親クラスを持つことが制限されます。

public class WorkerThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " が実行開始");
    }

    public static void main(String[] args) {
        WorkerThread task = new WorkerThread();
        task.start();
    }
}

2. Runnable インターフェースを実装する

リソース共有を容易にし、デザインパターンの適用にも適しています。スレッドインスタンス生成時にタスクを渡します。

public class TaskRunner implements Runnable {
    @Override
    public void run() {
        System.out.println("TaskRunner がタスクを実行しました");
    }

    public static void main(String[] args) {
        Runnable target = new TaskRunner();
        Thread t = new Thread(target);
        t.start();
    }
}

3. Callable と FutureTask を使用する

処理結果を返却する必要があり、例外もキャッチできない場合に有効です。非同期処理の値を受け取るための仕組みを提供します。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ReturnableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("非同期計算開始");
        Thread.sleep(2000);
        return "計算完了:1 + 1 = 2";
    }

    public static void main(String[] args) throws Exception {
        Callable<String> callable = new ReturnableTask();
        FutureTask<String> future = new FutureTask<>(callable);
        
        Thread worker = new Thread(future);
        worker.start();

        // 結果取得時にスレッドが完了するまで待機
        String result = future.get();
        System.out.println(result);
    }
}

スレッド操作メソッドの違い

スレッドの動作制御において、類似した挙動を持つメソッドがありますが、内部動作とロック解放の有無が決定的な違いとなります。

比較概要

  • sleep(long ms): 現在のスレッドを指定時間停止させます。ただし保持しているロック(シンクロナイザー)は維持されます。CPU チェックインが行われずに待機するため、他のスレッドはロックを獲得できません。
  • yield(): 実行可能なキューに戻り、優先順位の低いスレッドに CPU タイムスロットを譲る提案を行います。強制ではなく、次のタイミングで即座に再スケジューリングされる可能性もあります。
  • wait()/notify(): オブジェクト監視用のメソッドで、必ず synchronized ブロック内で呼び出す必要があります。wait() はロックを解放し、notify()/notifyAll() で唤醒されます。notify() は単一の待ちキューにあるスレッドを一つだけ選抜しますが、通知対象のスレッド数が不明な場合は notifyAll() を推奨します。
  • join(): 特定のスレッドが完了するまで、現在のスレッドの処理を一時停止(待機)します。

利用例:yield と sleep

public class ControlExample implements Runnable {
    public void run() {
        Thread.yield(); // 他のスレッドに処理機会を譲る
        try {
            // 3 秒間休止(中断時は例外発生)
            Thread.sleep(3000); 
            System.out.println(Thread.currentThread().getName() + " 継続");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        new Thread(new ControlExample()).start();
    }
}

利用例:wait と notify

ロック確保状態でのみ待機できます。

public class WaitNotifyEx implements Runnable {
    private final Object lock = new Object();

    @Override
    public void run() {
        synchronized (lock) {
            try {
                System.out.println(lock.hashCode() + " で待機開始");
                lock.wait(); // ロック解放状態で待機
                System.out.println(lock.hashCode() + " 再開");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyEx ex = new WaitNotifyEx();
        Thread t1 = new Thread(ex);
        t1.start();
        
        Thread.sleep(2000); // コール側も同期的に実行する場合の準備
        
        synchronized (ex.lock) {
            ex.lock.notify(); // 待機中のスレッド一つを喚起
        }
    }
}

利用例:join

複数の処理結果を揃えてから次のステップに進む際に使用します。

public class JoinDemo implements Runnable {
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("子スレッド終了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread sub = new Thread(new JoinDemo());
        sub.start();
        sub.join(); // 子スレッドの完了を待つ
        System.out.println("メイン処理:サブ完了確認後実行");
    }
}

インターラプト処理

interrupt() メソッドは、スレッドに中断フラグ(true)を設定します。実際のアクションはスレッドの現在の状態によって異なります。

  1. 通常実行時: フラグのみ true に設定され、処理は継続します。アプリ側でフラグを確認して自発的に停止する必要があります。
  2. 待機状態(wait, sleep, join): 例外 InterruptedException を抛出し、フラグをクリアします。

効果的な停止のためには、この例外を捕捉して処理を終了させるロジックを実装することが一般的です。

public class InterruptDemo implements Runnable {
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("スレッド継続中");
        } catch (InterruptedException e) {
            System.out.println("スレッド中止要求受信:" + Thread.currentThread().isAlive());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new InterruptDemo());
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 休眠中に中断を通知すると例外が送出される
    }
}

デーモンスレッドの設定

JVM プロセスを終了させる際、ユーザープロセスがすべて終了したら JVM も終了します。これを阻止するために作られたのがデーモンスレッドです。メインタスクのバックグラウンドサポート用として利用します。スレッド起動前に setDaemon(true) を呼ばないと例外になります。

public class DaemonEx implements Runnable {
    public void run() {
        try {
            Thread.sleep(10000);
            System.out.println("デーモン実行");
        } catch (InterruptedException e) {}
    }

    public static void main(String[] args) throws InterruptedException {
        Thread dt = new Thread(new DaemonEx());
        dt.setDaemon(true); // ユーザーでないスレッドに設定
        dt.start();
        
        System.out.println("メイン終了待機");
        Thread.sleep(2000);
        System.out.println("プログラム終了");
    }
}

上記のコードでは、メインスレッド(ユーザースレッド)が早く終了するため、デーモンスレッドが完走する前に JVM がシャットダウンされ、出力結果として「デーモン実行」は見られない可能性があります。

タグ: Java 並行処理,スレッド管理,FutureTask 同期制御

6月6日 21:23 投稿