Java における ReentrantLock の実装メカニズムと内部挙動

概要

JUC(java.util.concurrent)パッケージに存在するReentrantLockは、同期処理を制御するための主要な実装の一つである。基本的な機能は組み込みキーワードである Synchronized と類似しているが、高度な制御能力や柔軟性を提供するために設計されている。

Lock インターフェースと基本利用法

ReentrantLock は Lock インターフェースを実装しており、排他制御のために以下の主要なメソッドを利用する。

  • lock(): 取得できない場合スレッドをブロックし、成功まで待機する。
  • lockInterruptibly(): 割り込みを検知可能な状態でロックを獲得しようとする。
  • tryLock(): ノンブロッキングで試行し、失敗時は即座に false を返す。
  • tryLock(time, unit): 指定時間内の獲得を試み、タイムアウト時のハンドリングが可能。
  • unlock(): ロックの解放を行う。
// 典型的な使用パターン
final Lock resourceGuard = new ReentrantLock(false); // 非公平鎖として初期化

public void criticalSectionMethod() {
    resourceGuard.lock();
    try {
        // 保護したいロジック処理
        processSensitiveData();
    } finally {
        // 必ず解放すること
        resourceGuard.unlock();
    }
}

公平性と非公平性の設定

コンストラクタによりロックの動作ポリシーを選択できる。引数なしの場合または false を渡すと「非公平モード」になり、true を渡すと「公平モード」となる。

// デフォルトは NonfairSync を使用
public ReentrantLock() {
    this.sync = new NonfairSync();
}

// 公平フラグによる切り替え
public ReentrantLock(boolean isFair) {
    this.sync = isFair ? new FairSync() : new NonfairSync();
}

公平性は待ち行列が FIFO(先進先出)となり、長い間待たされたスレッドを優先する。一方、非公平性は競合時にスリープ状態のスレッドを含め、CPU スケジューリング次第で誰が獲得してもよい仕組みである。

内部構造と AQS への依存

ReentrantLock の核心的なロジックは内部クラスである Sync に集約されており、これは AQS(AbstractQueuedSynchronizer)を継承している。加減速および状態管理は AQS が提供するテンプレートメソッドに従う。

// 外部から見えるメソッドは内部クラスに委譲
public final void lock() {
    this.sync.acquireLockProcedure();
}

public final boolean tryAttemptLock() {
    return this.sync.nonFairTryToGet(1);
}

public final void releaseLock() {
    this.sync.performRelease(1);
}

公平ロックの実装フロー

FairSync クラスでは、スレッドがロックを取得する前に待機キューの順序を確認する。

protected final boolean attemptAcquire(int requestCount) {
    Thread currentThread = Thread.currentThread();
    int currentState = getState();

    if (currentState == 0) {
        // キュー内で先行するノードが存在しなければ取得を試みる
        if (!hasPredecessors() && casState(0, requestCount)) {
            setOwnerThread(currentThread);
            return true;
        }
    } else if (currentThread == getOwnerThread()) {
        // 可重入性:同一スレッドによる再入りを許可
        int nextState = currentState + requestCount;
        if (nextState < 0) throw new Error("Lock count overflow");
        setState(nextState);
        return true;
    }
    return false;
}

このコードが示す通り、!hasPredecessors() のチェックにより、先着順の保証がなされている。

非公平ロックの仕組み

NonFairSync では、初期段階でのキュー確認を省略することで、より高速な獲得を目指す。

final void fastAcquireLock() {
    // 最初に直接 CAS でステート変更を試みる
    if (casState(0, 1)) {
        setOwnerThread(Thread.currentThread());
    } else {
        // 失敗した場合のみ通常のアキューアプロセスへ遷移
        acquire(1);
    }
}

非公平モードでは、新しいスレッドが到着した際、既にキューに入っているスレッドよりも先にロックを獲得できる可能性があるため、スループット向上が期待される。

ロックの解放処理

解除時は共有ステートの値を減少させる。カウントがゼロになった時点で初めて完全に解放され、所有者情報がクリアされる。

protected final boolean performRelease(int permits) {
    int remainingState = getState() - permits;
    
    if (Thread.currentThread() != getExclusiveOwner()) {
        throw new IllegalMonitorStateException();
    }

    boolean fullyReleased = false;
    if (remainingState == 0) {
        fullyReleased = true;
        setExclusiveOwner(null);
    }
    
    setState(remainingState);
    return fullyReleased;
}

ここで重要なのは、複数の acquire に対して 1 回の release しかない場合、ステートはゼロにならず、ロックは有効であり続ける点だ。

Synchronized との相違点

両者とも再入可能かつ排他的だが、実装面と運用面で以下のような違いがある。

特徴 Synchronized ReentrantLock
解放方式 自動(言語構文レベル) 手動(finally ブロック推奨)
公平性設定 不可(非公平) 選択可能(デフォルト非公平)
中断応答 不可 可能(lockInterruptibly)
基盤技術 オブジェクト Monitor AQS クラス体系
Condition 通知 wait/notify newCondition による細かな制御

注意点とベストプラクティス

明示的な可重入性管理

Synchronized の暗黙的カウントに対し、ReentrantLock は明確な加減算モデルを採用している。したがって、ロック取得と解放のカウント数が一致していないと、他のスレッドが永遠にブロックされる状態を引き起こす。

公平性のトレードオフ

公平モードは順番を保証するが、スレッド切り替えのオーバーヘッドが増大するため、一般的にはパフォーマンスが低下する傾向がある。非公平モードは飢え状態(Starvation)のリスクはあるものの、高い処理能力を提供する。

タグ: Java ReentrantLock AQS concurrency thread-safety

5月19日 20:42 投稿