スレッドの同期と join メソッド
マルチスレッド環境において、特定のスレッドが別のスレッドの終了を待つ必要がある場合があります。Java の Thread クラスが提供する join() メソッドは、この協調動作を実現するための主要な手段です。呼び出し元のスレッドは、対象のスレッドが run() メソッドの実行を完了するまで、自身の処理を一時停止します。
join() メソッドには、タイムアウトを指定できるオーバーロードも存在します。パラメータを指定しない場合、対象スレッドが終了するまで無期限に待機します。ミリ秒単位で待機時間を制限する場合は、引数として時間を与えます。指定時間が経過しても対象スレッドが終了していない場合、待ち状態は解除され、両スレッドは並行して実行を続けます。
以下の例では、PrimaryWorker が起動後に SecondaryWorker を生成し、その終了を待ってから自身の実行を終了する流れを示しています。
package com.example.concurrent;
public class ThreadSyncDemo {
public static void main(String[] args) {
PrimaryWorker mainTask = new PrimaryWorker();
mainTask.start();
}
}
class PrimaryWorker extends Thread {
@Override
public void run() {
System.out.println("プライマリタスク:開始");
SecondaryWorker subTask = new SecondaryWorker();
subTask.start();
try {
// subTask の終了を待つ
// subTask.join(2000); // タイムアウト付きの場合
subTask.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("プライマリタスク:終了");
}
}
class SecondaryWorker extends Thread {
@Override
public void run() {
System.out.println("セカンダリタスク:開始");
try {
// 処理時間のシミュレーション
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("セカンダリタスク:終了");
}
}
スレッド間のロックと依存関係
スレッドの実行順序を制御する際、join だけでなく、オブジェクトの監視ロック( synchronized )が影響を与えることがあります。あるスレッドが別のスレッドが保持するロックを必要とする場合、適切な順序でロックが解放されない限り、処理がブロックされます。
例えば、スレッド A がスレッド B の終了を待っており(join)、同時にスレッド C がスレッド B のオブジェクト自体をロックしている場合、スレッド A はスレッド C がロックを解放するまで再開できません。このような依存関係が複雑化すると、予期せぬ待機状態や停止を引き起こす可能性があります。
package com.example.concurrent;
public class LockingScenario {
public static void main(String[] args) {
WorkerThread target = new WorkerThread();
target.start();
LockerThread locker = new LockerThread(target);
locker.start();
}
}
class WorkerThread extends Thread {
@Override
public void run() {
System.out.println("ワーカー:処理開始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ワーカー:処理終了");
}
}
class LockerThread extends Thread {
private final WorkerThread targetWorker;
public LockerThread(WorkerThread worker) {
this.targetWorker = worker;
}
@Override
public void run() {
System.out.println("ロッカー:ロック取得試行");
synchronized (targetWorker) {
System.out.println("ロッカー:ロック取得済み");
try {
// ロックを保持したまま時間を消費
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ロッカー:ロック解放");
}
}
}
デッドロックの条件と回避策
複数のスレッドが互いに必要なリソースを待ち続け、永遠に処理が進まなくなる状態を「デッドロック」と呼びます。典型的な例として、哲学者の食事問題が挙げられます。複数のプロセスが共有リソースを巡って競合し、循環待ち状態に陥ることが原因です。
デッドロックが発生するには、主に以下の条件が同時に成立する必要があります。
- 相互排除:リソースが同時に複数のスレッドによって使用できない。
- 保持と待機:スレッドが少なくとも一つのリソースを保持したまま、他のリソースを待つ。
- 横取り不可:保持しているリソースを強制的に奪えない。
- 循環待機:スレッド間でリソースの待ち関係が循環している。
これを防ぐためには、共有リソースへのアクセスを最小限にする、ロックを取得する順序を統一する、あるいは sleep や join を用いて実行タイミングをずらすなどの対策が有効です。
Timer クラスによる定期実行
特定の時刻から定期的にタスクを実行したい場合、java.util.Timer クラスが利用できます。これはバックグラウンドスレッドで動作し、指定された遅延後、または指定された時刻から一定間隔で TimerTask を実行します。
schedule メソッドを使用することで、初回実行時刻と実行間隔(ミリ秒)を指定できます。匿名内部クラスを用いてタスクの内容を定義するのが一般的です。
package com.example.concurrent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class ScheduledExecution {
private static int executionCount = 0;
public static void main(String[] args) {
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 実行開始時刻の設定(例:現在の 5 秒後)
Date startTime = new Date(System.currentTimeMillis() + 5000);
System.out.println("スケジューラ起動: " + sdf.format(new Date()));
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("タスク実行回数:" + executionCount++);
System.out.println("現在時刻:" + sdf.format(new Date()));
}
}, startTime, 4000); // 初回時刻から 4 秒間隔で実行
}
}