yohhoyの日記

技術的メモをしていきたい日記

java.util.concurrent.locks.StampedLock同期プリミティブ

Java 8で追加された java.util.concurrent.locks.StampedLock 同期プリミティブについてメモ。

まとめ:

  • StampedLockクラスは、「楽観的(optimistic)Read操作」および「ロック昇格(upgrade lock)」を追加サポートしたReader-Writerロックとして機能する。
  • 注意:StampedLockは再入可能でない(non-reentrant)。また悲観的Readロック/WriteロックLockインタフェースからの条件変数Condition取り出しはサポートしない。

楽観的Read操作(ロックフリーRead操作)

Java 5で導入されたReader-WriterロックReentrantReadWriteLockクラスは、悲観的(pessimistic) Readロック/Writeロック操作のみを提供する。悲観的ロック操作ではロック獲得要求競合時の優先付けにより、Write操作要求スレッド/Read操作要求スレッドが飢餓状態(starvation)に陥る可能性がある。*1

  • Readロック優先:複数スレッドによる連続的なReadロック保持により、Writeロック獲得要求スレッドの進行がブロックされる。[Java 5]
  • Writeロック優先:高頻度でのWriteロック獲得により、Readロック獲得要求スレッドの進行が遅延する。[Java 6以降]

Java 5の振る舞いに比べるとJava 6の振る舞いは改善されているが、依然としてRead操作のスケーラビリティは阻害される。

StampedLockクラスは、ロック取得を必要としない(ロックフリー)楽観的なRead操作をサポートする。また、通常の悲観的Readロック操作/Writeロック操作インタフェースも提供する。

楽観的Read操作の実装パターンを下記に示す(JDKドキュメントより改変引用)。

class Point {
  private double x, y;
  private final StampedLock sl = new StampedLock();

  double distanceFromOrigin() {
    // 楽観的Read操作の開始(タイムスタンプ取得)
    long stamp = sl.tryOptimisticRead();
    // 競合可能性のある変数x,yのRead操作
    double currentX = x, currentY = y;
    // 競合Readの完了後にタイムスタンプ再チェック
    if (!sl.validate(stamp)) {
      // Writeロック獲得によりタイムスタンプ更新されるため
      // 競合Writeを検知し悲観的Readロックへフォールバック
      stamp = sl.readLock();
      try {
        currentX = x;
        currentY = y;
      } finally {
        sl.unlockRead(stamp);
      }
    }
    // ローカル変数currentX/Yを使用
  }
}

注意:楽観的Read操作内では副作用のない(side-effect-free)操作のみ行うこと。また、Read操作の競合によって一貫性の無い状態を読み取る可能性に留意すること。(前掲コードでは競合検知で悲観的Readロックへフォールバック動作させている)

ロック昇格

従来のReader-WriterロックReadWriteLockインタフェース(ReentrantReadWriteLockクラスは同インタフェースを実装)は、排他的Writeロックから悲観的Readロックへのロック降格(downgrade lock)のみサポートしている。

StampedLockクラスは、悲観的Readロックから排他的Writeロックへのロック昇格操作をサポートする。ロック降格では常に排他的Writeロック獲得の必要があったが、ロック昇格によって “変数Read操作からの条件付き変数Write操作” を効率的に実装できる。

ロック昇格の実装パターンを下記に示す(JDKドキュメントより改変引用)。

class Point {
  private double x, y;
  private final StampedLock sl = new StampedLock();

  void moveIfAtOrigin(double newX, double newY) {
    // 悲観的Readロック獲得 
    long stamp = sl.readLock();
    try {
      while (x == 0.0 && y == 0.0) {
        // 排他的Writeロックへの昇格チェック
        long ws = sl.tryConvertToWriteLock(stamp);
        if (ws != 0L) {
          // [A]Writeロック獲得済みのため変数Write操作可
          stamp = ws;
          x = newX;
          y = newY;
          break;
        }
        else {
          // [B]Writeロック昇格失敗時は...
          // 1.獲得済みのReadロック解放
          sl.unlockRead(stamp);
          // 2.改めて排他的Writeロック獲得
          stamp = sl.writeLock();
          // 3.次ループ反復でtryConvertToWriteLockは非0を返す
        }
      }
    } finally {
      // ReadロックまたはWriteロックを解放
      sl.unlock(stamp);
    }
  }
}

関連URL