【Java学習|実務向け】Java並行処理の現場力:Conditionを用いたスレッド間協調制御の極意

導入

Javaでのマルチスレッドプログラミングにおいて、単純な排他制御(synchronizedやReentrantLock)だけでは解決できないのが「特定条件が整うまでスレッドを待機させ、別のスレッドから通知を受けて再開する」という処理です。これらを制御するための強力なツールが java.util.concurrent.locks.Condition です。適切に使うことで、生産者・消費者問題のような複雑な協調処理を、効率的かつ安全に実装できるようになります。

基礎知識

Conditionは、ReentrantLockと組み合わせて使用します。主なメソッドは以下の3つです。

await(): 現在のスレッドを一時停止し、ロックを解放して待機状態に入ります。
signal(): 待機中のスレッドのうち、1つをランダムに起こします。
signalAll(): 待機中のすべてのスレッドを起こします。

従来のObjectクラスのwait/notify/notifyAllと比較すると、複数のConditionインスタンスを生成できるため、待機条件を細かく分離できる(例:空状態の待ちと満杯状態の待ちを分ける)という大きなメリットがあります。

実装/解決策

Conditionを使う際は、必ず「whileループ内でawaitを呼ぶ」ことが鉄則です。これを怠ると、別のスレッドが通知する前にスレッドが誤って再開してしまう「スプリアス・ウェイクアップ(Spurious Wakeup)」によって、予期せぬバグが発生します。

サンプルプログラム

以下は、容量制限のあるバッファ(キュー)を実装した例です。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SimpleBuffer {
private final Queue buffer = new LinkedList<>();
private final int CAPACITY = 5;
private final Lock lock = new ReentrantLock();
// 満杯待ち用と空待ち用のConditionを分けるのがポイント
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

public void put(int value) throws InterruptedException {
lock.lock();
try {
// 満杯なら待機。whileを使うのが鉄則
while (buffer.size() == CAPACITY) {
notFull.await();
}
buffer.add(value);
System.out.println(“追加: ” + value);
// データが入ったので、空待ちしているスレッドへ通知
notEmpty.signal();
} finally {
lock.unlock();
}
}

public int take() throws InterruptedException {
lock.lock();
try {
// 空なら待機
while (buffer.isEmpty()) {
notEmpty.await();
}
int value = buffer.remove();
System.out.println(“取得: ” + value);
// 枠が空いたので、満杯待ちしているスレッドへ通知
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}

応用・注意点

現代のJava開発では、Virtual Threads(Project Loom)やStructured Concurrencyの導入により、低レイヤーのロック制御を直接書く機会は減りつつあります。しかし、独自のデータ構造や、高負荷な非同期タスクの制御を行う際には、依然としてConditionは不可欠です。

注意点として、signalAll()は強力ですが、すべてのスレッドを叩き起こしてロック獲得競争を発生させるため、スレッド数が非常に多い場合はパフォーマンスが低下する可能性があります。状況に応じてsignal()と使い分けてください。また、最近の非同期処理では、CompletableFutureやFlow APIのようなリアクティブなアプローチも検討し、本当に「スレッドを待機させる必要があるか」を設計段階で再考することも重要です。

コメント

タイトルとURLをコピーしました