ReentrantLockの概要と並行プログラミングにおける役割
現代のソフトウェア開発において、マルチコアプロセッサの能力を最大限に引き出す並行プログラミングは不可欠な技術です。Javaでは標準的な同期手段としてsynchronizedキーワードが提供されていますが、より柔軟で高度な制御が必要な場合にはjava.util.concurrent.locks.ReentrantLockが利用されます。
ReentrantLockは、再入可能性(同じスレッドが保持しているロックを再度取得できる特性)を持ち、明示的なロックの取得と解放を行うためのAPIを提供します。これにより、タイムアウト付きのロック取得、割り込み可能なロック、公平性ポリシーの設定など、標準の同期ブロックでは不可能な高度なスレッド制御が可能になります。
ReentrantLockの基本メカニズム
ReentrantLockの内部動作は、主にAbstractQueuedSynchronizer (AQS) に依存しています。これは、スレッドの待機キューを管理するためのフレームワークであり、CAS(Compare-And-Swap)操作を利用してステートを更新します。主な特徴は以下の通りです。
- 再入可能性: スレッドが既に保持しているロックを再度要求した際、デッドロックを起こさずにカウントをインクリメントして進行できます。
- 公平性の選択: コンストラクタで
trueを指定することで、待機時間の長いスレッドに優先的にロックを割り当てる公平モードを選択できます(デフォルトは非公平モード)。 - 柔軟な制御:
tryLock()による非ブロック的な試行や、lockInterruptibly()による割り込み対応が可能です。
基本的な使用方法と実装例
ReentrantLockを使用する際は、例外が発生した場合でも確実にロックを解放するために、必ずtry-finallyブロックを使用するのがベストプラクティスです。
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SecureCounter {
private static final Logger log = LoggerFactory.getLogger(SecureCounter.class);
private final ReentrantLock lock = new ReentrantLock();
private int stockLevel = 100;
public void deductStock(int amount) {
lock.lock(); // ロックの取得
try {
if (stockLevel >= amount) {
stockLevel -= amount;
log.info("在庫を減らしました。現在の在庫: {}", stockLevel);
}
} finally {
lock.unlock(); // 確実に解放
}
}
}
デッドロックの回避とtryLockの活用
複数のロックを扱う場合、取得順序の不整合によってデッドロックが発生するリスクがあります。ReentrantLockのtryLock()を使用することで、一定時間待機してもロックが取れない場合に処理を諦める、あるいは別のロジックを実行するといった回避策を講じることができます。
public void transfer(ReentrantLock fromLock, ReentrantLock toLock) {
while (true) {
boolean gotFrom = fromLock.tryLock();
boolean gotTo = toLock.tryLock();
try {
if (gotFrom && gotTo) {
// 両方のロックを取得できた場合の処理
return;
}
} finally {
if (gotFrom) fromLock.unlock();
if (gotTo) toLock.unlock();
}
// 再試行前の待機(ライブロック防止)
try { Thread.sleep((long) (Math.random() * 50)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
Conditionインターフェースによるスレッド間通信
ReentrantLockは、Object.wait()やnotify()に代わる高度な待機・通知メカニズムとしてConditionオブジェクトを提供します。これにより、特定の条件に基づいてスレッドを効率的に待機させることができます。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SharedBuffer {
private final Lock lock = new ReentrantLock();
private final Condition isNotFull = lock.newCondition();
private final Condition isNotEmpty = lock.newCondition();
private final Object[] items = new Object[10];
private int count, putIndex, takeIndex;
public void produce(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
isNotFull.await(); // バッファが空くまで待機
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
isNotEmpty.signal(); // 消費者に通知
} finally {
lock.unlock();
}
}
public Object consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
isNotEmpty.await(); // データが入るまで待機
}
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
isNotFull.signal(); // 生産者に通知
return x;
} finally {
lock.unlock();
}
}
}
ReentrantReadWriteLockによる最適化
読み取り操作が書き込み操作よりも圧倒的に多い場合、ReentrantReadWriteLockを使用することでパフォーマンスを大幅に向上させることができます。これは「共有(読み取り)ロック」と「排他(書き込み)ロック」を分離して管理します。
- 読み取りロック: 他の書き込みスレッドがいない限り、複数のスレッドが同時に保持できます。
- 書き込みロック: 唯一のスレッドのみが保持でき、他の読み取り・書き込みをすべてブロックします。
import java.util.concurrent.locks.ReentrantReadWriteLock;
class CacheSystem {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private String cachedData = "";
public String read() {
rwLock.readLock().lock();
try {
return cachedData;
} finally {
rwLock.readLock().unlock();
}
}
public void update(String data) {
rwLock.writeLock().lock();
try {
this.cachedData = data;
} finally {
rwLock.writeLock().unlock();
}
}
}
パフォーマンス最適化と注意点
ReentrantLockを効果的に利用するためのガイドラインは以下の通りです。
- ロックの粒度を最小化する: 同期が必要な最小限のコードブロックのみをロックで囲みます。長時間実行されるI/O処理などはロックの外に配置すべきです。
- 公平性設定の慎重な選択: 公平モードはキューの管理オーバーヘッドが大きく、スループットが低下するため、厳格な順序が必要な場合以外は非公平モード(デフォルト)を使用します。
- 並行コレクションの検討: 独自のロックロジックを組む前に、
ConcurrentHashMapやCopyOnWriteArrayListなど、Java標準の並行コレクションで解決できないか検討してください。 - スレッドリークの防止:
unlock()が呼ばれないパスが存在すると、システム全体が停止する可能性があります。常にfinallyブロックでの解放を徹底します。