Th-06: ReentrantLock / ReadWriteLock
synchronized はシンプルですが、タイムアウト指定やロック取得の試みといった細かい制御はできません。ReentrantLock を使うと tryLock() でタイムアウトを設定でき、ReadWriteLock を使うと読み取りと書き込みを分離してパフォーマンスを向上させられます。
synchronized との違い
synchronized はシンプルで使いやすい反面、ロック待ちが無限に続く可能性があります。ReentrantLock は tryLock(timeout) でタイムアウトを設定でき、 指定時間内にロックを取れなければ別の処理に切り替えることができます。 また、公平モード(順番を守る)の設定も可能です。
| 機能 | synchronized | ReentrantLock |
|---|---|---|
| 基本的なロック | ✅ | ✅ |
| タイムアウト付き tryLock | ❌ | ✅ |
| 公平モード(FIFO) | ❌ | ✅(new ReentrantLock(true)) |
| 読み書き分離 | ❌ | ✅(ReadWriteLock) |
| 記述のシンプルさ | ◎ | △(unlock() を finally に書く必要あり) |
ReadWriteLock の読み書き分離
キャッシュのように「読み取りは頻繁・書き込みはまれ」という場面では、ReadWriteLock が効果的です。 読み取りロックは複数スレッドが同時に取得できますが、書き込みロックは1スレッドのみが排他的に取得します。 これにより読み取り専用のアクセスをブロックせず、スループットを高められます。
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 複数スレッドが同時取得可
Lock writeLock = rwLock.writeLock(); // 1スレッドのみ取得可
// 読み取り: 複数スレッドが同時に実行できる
readLock.lock();
try { /* データを読む */ } finally { readLock.unlock(); }
// 書き込み: 他のすべてのスレッドが待機する
writeLock.lock();
try { /* データを更新 */ } finally { writeLock.unlock(); }サンプルコード
よくあるミス・注意点
⚠️ lock() 後に unlock() を finally に書かないと例外でロックが解放されない
lock() を呼んだ後、処理中に例外が発生すると unlock() が呼ばれずロックが永遠に保持されます。 他のスレッドはロックを取得できなくなり、デッドロック状態になります。unlock() は必ず finally ブロックに書いてください。
// ❌ finally なし: 例外が起きるとロックが解放されない
lock.lock();
count++; // ここで例外が起きると unlock() が呼ばれない
lock.unlock();
// ✅ finally に unlock() を書く
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 例外が起きても必ず解放される
}⚠️ 読み取りロックを保持したまま書き込みロックを取得しようとするとデッドロックになる
ReadWriteLock では、同一スレッドが読み取りロックを保持したまま書き込みロックを取得しようとするとデッドロックになります。 読み取りロックを解放してから書き込みロックを取得するか、最初から書き込みロックを使ってください。
テストする観点
- ✅
ReentrantLockで 5スレッド × 1000回インクリメントした結果が必ず 5000 になること - ✅
tryLock(timeoutMs)で指定時間内にロックを取れない場合にfalseが返ること - ✅
ReadWriteLockで複数の読み取りスレッドが同時実行できること(書き込みスレッドはブロックされること) - ✅ 書き込み中は読み取りスレッドが待機し、書き込み完了後に新しい値を読み取れること
- ✅
unlock()を呼ばずに例外が起きた場合、他スレッドがロックを永遠に待ち続けること(finally の重要性確認)