Javaにおけるスレッドの実装と同期制御

Javaでは、複数のタスクを並行して実行するためにスレッド(Thread)が重要な役割を果たします。プロセスはアプリケーションの実行単位であり、その内部で複数のスレッドが独立して動作することで、効率的なリソース利用と応答性の向上が可能になります。

プロセスとスレッドの関係

プロセスは、メモリ上にロードされたプログラムの実行インスタンスです。一つのプロセスは、少なくとも一つのスレッド(メインスレッド)を含み、複数のスレッドを生成して同時に処理を進めることができます。すべてのスレッドは同一のプロセス内に存在し、メモリ空間やファイルハンドルなどのリソースを共有します。

スレッドの生成方法

1. Threadクラスの継承

Threadクラスを拡張し、run()メソッドをオーバーライドすることでスレッドを定義します。start()メソッドを呼び出すことで、スレッドが実行状態に移行します。

public class WorkerThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("WorkerThread: " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

// 使用例
WorkerThread worker = new WorkerThread();
worker.start();

2. Runnableインターフェースの実装

Javaは単一継承しか許可しないため、Runnableインターフェースを実装する方法が推奨されます。これにより、他のクラスを継承できる自由度が得られます。また、複数のスレッドが同じRunnableインスタンスを共有できるため、リソースの効率的な利用が可能です。

public class Task implements Runnable {
    private final String taskName;

    public Task(String name) {
        this.taskName = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(taskName + " - ステップ: " + i);
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

// 使用例
Task task1 = new Task("タスクA");
Task task2 = new Task("タスクB");

Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);

t1.start();
t2.start();

スレッドの基本操作

メソッド説明
start()スレッドを開始し、run()メソッドの実行をスケジュールする
getName()スレッドの名前を返す
setPriority(int)スレッドの優先度を設定(1〜10)
getPriority()スレッドの優先度を取得
join()対象スレッドの終了を待つ
sleep(long)現在のスレッドを指定ミリ秒だけ一時停止
currentThread()現在実行中のスレッドを返す

スレッド優先度とスケジューリング

Javaのスレッド優先度は1(MIN_PRIORITY)から10(MAX_PRIORITY)の範囲で、デフォルトは5(NORM_PRIORITY)です。優先度が高いスレッドはCPUの割り当てを優先されますが、OSのスケジューラに依存するため、厳密な実行順序は保証されません。時間分割(time-slicing)方式により、同レベルのスレッドは公平にCPUを共有します。

スレッドのライフサイクル

  • 新建(New): Threadオブジェクト生成後、start()呼び出し前
  • 可実行(Runnable): start()呼び出し後、CPUの割り当てを待機中
  • 実行中(Running): CPUを取得し、run()メソッドが実行中
  • ブロッキング(Blocked): I/O待機、sleep()、wait()などにより一時停止
  • 終了(Terminated): run()完了、例外発生、またはstop()呼び出し

複数スレッドの利用メリットと課題

  • メリット: I/O待ち時間の有効活用、CPUコアの並列利用、UIの応答性向上
  • 課題: メモリ消費増加、スレッド管理の複雑化、共有リソースへの競合

同期制御(Synchronization)

複数スレッドが同じ変数やオブジェクトを同時に書き込むと、データの不整合が発生します。これを防ぐために、同期ブロックまたは同期メソッドを用います。

// 同期ブロック
Object lock = new Object();

new Thread(() -> {
    synchronized (lock) {
        // 共有リソースへのアクセス
        System.out.println("スレッドAがロックを獲得");
    }
}).start();

new Thread(() -> {
    synchronized (lock) {
        // 他のスレッドがロックを解放するまで待機
        System.out.println("スレッドBがロックを獲得");
    }
}).start();

// 同期メソッド
public synchronized void updateCounter() {
    counter++;
}

ReentrantLockによる明示的ロック

JDK 5以降、java.util.concurrent.locks.Lockインターフェースを用いたより柔軟な同期制御が可能になります。synchronizedと異なり、ロックの取得・解放を明示的に制御できます。

import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lock = new ReentrantLock();

new Thread(() -> {
    lock.lock();
    try {
        System.out.println("ロック取得: " + Thread.currentThread().getName());
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock.unlock(); // 絶対に解放すること
    }
}).start();

スレッド間通信

同期ブロック内でのみ有効な、スレッド間の通知・待機メカニズムです。すべてのメソッドはObjectクラスに定義されています。

  • wait(): ロックを解放し、スレッドを待機状態に移行
  • notify(): 同一ロックを待機中のうち一つのスレッドを再開
  • notifyAll(): すべての待機スレッドを再開
public class ProducerConsumer {
    private final Object lock = new Object();
    private boolean produced = false;

    public void produce() {
        synchronized (lock) {
            while (produced) {
                try {
                    lock.wait(); // 生産者待機
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("製品を生産");
            produced = true;
            lock.notify(); // 消費者を起動
        }
    }

    public void consume() {
        synchronized (lock) {
            while (!produced) {
                try {
                    lock.wait(); // 消費者待機
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("製品を消費");
            produced = false;
            lock.notify(); // 生産者を起動
        }
    }
}

CallableとFutureによる結果返却

Runnableは戻り値を持てませんが、Callableは戻り値と例外処理をサポートします。結果を取得するにはFutureTaskを介します。

import java.util.concurrent.*;

Callable<Integer> task = () -> {
    int sum = 0;
    for (int i = 1; i <= 10; i++) {
        sum += i;
        Thread.sleep(100); // 模擬的な処理時間
    }
    return sum;
};

FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();

try {
    Integer result = future.get(); // 結果をブロッキングで取得
    System.out.println("合計: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

タグ: Java スレッド 同期 ReentrantLock Callable

7月25日 03:31 投稿