導入:なぜReadWriteLockが必要なのか
Javaでマルチスレッド処理を行う際、synchronizedブロックを使って共有リソースを保護することは一般的です。しかし、synchronizedは「読み取り」と「書き込み」を区別せず、常に排他的にロックします。読み取り処理が頻繁に発生するシステムでこれを行うと、本来同時に実行可能なはずの読み取り処理まで直列化され、深刻なボトルネックとなります。この課題を解決するのが java.util.concurrent.locks.ReadWriteLock です。
基礎知識:ReadWriteLockの仕組み
ReadWriteLockは、読み取り用のロック(ReadLock)と書き込み用のロック(WriteLock)を分離して管理します。
・読み取りロック:他のスレッドが読み取りロックを保持していても、さらに読み取りロックを取得可能です。ただし、書き込みロックは取得できません。
・書き込みロック:排他的です。他のスレッドが読み取り中、または書き込み中であれば、ロックを取得できません。
これにより、「読み取りは並列化し、書き込み時のみ安全を担保する」という理想的な並行制御が可能になります。
実装・解決策
実装には、ReadWriteLockインターフェースの標準的な実装クラスである ReentrantReadWriteLock を使用します。基本戦略は以下の通りです。
1. 読み取りメソッドには readLock() を使用。
2. 書き込みメソッドには writeLock() を使用。
3. try-finally ブロックを必ず使用し、ロックの解放漏れ(デッドロックの原因)を防ぐ。
サンプルプログラム
以下のコードは、スレッドセーフなデータ保持クラスの実装例です。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedData {
private int value = 0;
// ロックオブジェクトの生成
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public int getValue() {
// 読み取りロックを取得
rwLock.readLock().lock();
try {
return value;
} finally {
// 必ず解放する
rwLock.readLock().unlock();
}
}
public void increment() {
// 書き込みロックを取得(他の読み取り/書き込みをブロック)
rwLock.writeLock().lock();
try {
value++;
} finally {
// 必ず解放する
rwLock.writeLock().unlock();
}
}
}
応用・注意点
現場で活用する際の重要な注意点を挙げます。
1. 書き込みロックの格下げ(Downgrading):ReentrantReadWriteLockは、書き込みロックを保持したまま読み取りロックを取得することは可能ですが、逆(読み取りから書き込みへ昇格)はできません。これはデッドロックを招くためです。
2. Virtual Threadsとの相性:Java 21から導入されたVirtual Threads環境下でも、このロックは有効です。しかし、ロック保持期間が長すぎるとスレッドがピン留めされる可能性があるため、ロック内では極力重い処理(外部API呼び出しなど)を行わないようにしましょう。
3. 公平性(Fairness):コンストラクタで new ReentrantReadWriteLock(true) と指定すると公平モードになります。書き込み待ちのスレッドが優先されやすくなりますが、スループットは低下するため、パフォーマンス要件と相談して決めてください。
読み取り頻度が高いキャッシュ機構や共有設定値の管理において、本手法は非常に強力な武器となります。ぜひ活用してください。

コメント