導入:なぜStampedLockが重要なのか
Javaでマルチスレッド処理を行う際、複数のスレッドからデータにアクセスする「競合」を防ぐためにロック(排他制御)は欠かせません。しかし、従来のReentrantReadWriteLock(読み書きロック)は、読み取り処理が多い環境ではオーバーヘッドが大きく、パフォーマンスのボトルネックになりがちでした。
Java 8で導入されたStampedLockは、この課題を解決するために登場しました。特に「読み取り」を重視した楽観的ロックという仕組みを提供することで、高負荷な環境でも高速な並行処理を実現します。
基礎知識:StampedLockの仕組み
StampedLockを理解するための重要なキーワードが「楽観的読み取り(Optimistic Reading)」です。
通常、ロックとは「他のスレッドを追い出す」行為ですが、StampedLockの楽観的読み取りでは「ロックせずに読み取る」ことを試みます。読み取り中に書き込みが発生していないかを後から確認し、もし書き込まれていたらその時初めて「悲観的(通常の)ロック」に昇格させるという戦略をとります。これにより、読み取り処理が頻発するシステムでの待ち時間を最小限に抑えられます。
実装/解決策:基本的な使い方
StampedLockを利用する際は、必ず「スタンプ(long値)」を管理する必要があります。これはロックの状態を示すIDのようなもので、ロックの解除や検証の際に必ず渡す必要があります。
サンプルプログラム
以下のコードは、共有データに対して読み取りと書き込みを行う基本的な実装例です。
import java.util.concurrent.locks.StampedLock;
public class StampedLockSample {
private double x, y;
private final StampedLock sl = new StampedLock();
// 書き込み処理:通常の排他ロック
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 書き込みロックを取得
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 書き込みロックを解放
}
}
// 読み取り処理:楽観的読み取り
public double distanceFromOrigin() {
// 1. まず楽観的読み取りを試みる(ロックはしない)
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
// 2. 読み取り中に書き込みが発生していないか検証
if (!sl.validate(stamp)) {
// 3. 書き込みが発生していたら、悲観的読み取りロックに昇格
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX currentX + currentY currentY);
}
}
応用・注意点:現場でのアドバイス
現場でStampedLockを使う際に注意すべきポイントが3つあります。
1. 再入不可能(非再入可能):ReentrantReadWriteLockとは異なり、StampedLockは「再入可能」ではありません。同じスレッドでロックを二重に取得しようとするとデッドロックを引き起こすため注意が必要です。
2. 条件変数がない:Condition(await/signalなど)をサポートしていません。複雑な待機条件が必要な場合は、従来のLockインターフェースを使用してください。
3. Virtual Threadsとの相性:Java 21以降のVirtual Threads(仮想スレッド)環境下では、ロックを保持したままブロック処理を行うと、OSスレッドが占有されてしまい、仮想スレッドのメリットが失われます。ロック内では重いI/O処理や長時間のスリープを避け、短時間で処理を終えるように設計するのがシニアエンジニアとしての鉄則です。
StampedLockは非常に強力な武器ですが、万能ではありません。読み取り頻度が極端に高い場合にのみ適用し、それ以外は標準のReentrantReadWriteLockやjava.util.concurrentの便利なコレクションクラスを活用することをおすすめします。

コメント