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();
}