自旋锁の原理と実装

自旋鎖(Spinlock)とは、ロックが取得できない際にスレッドをブロッキングせずに、ループ処理を通じて再三にわたってロックの取得を試みる同期機構です。

自旋と非自旋ロックの動作比較

自旋ロックと互渴望ロック(ミューテックスなど)の主な違いは、ロック取得失敗時の挙動にあります。

  • 自旋ロック: ロックを獲得するまで CPU を占有したまま、while や do-while などのループで継続的にチェックし続けます。 スレッド状態は常に Runnable のまま変化しないため、スレッド切り替えに伴うオーバーヘッドが発生しません。
  • 非自旋ロック(ブロッキングロック): ロック取得に失敗した際、スレッドをブロッキング(WAITING 状態に遷移)し、CPU を開放します。 ロック解放後にスケジューラによって再開されるため、状態遷移のコストがかかりますが、CPU 資源の無駄遣いを回避できます。

自旋锁の利点

スレッドの切り替え(コンテキストスイッチ)はコストが高い処理です。 特に、クリティカルセクションの実行時間がごく短い場合、ブロッキング+起床処理のコストが実態処理時間を上回ることがあります。 そのようなケースでは、/auto-spinning/により短時間の待機で済むため、全体の効率が向上します。

AtomicLong における自旋の実例

Java の java.util.concurrent.atomic.AtomicLong クラスの内部実装では、CAS(Compare-And-Swap)を用いた自旋処理が採用されています。 以下はその getAndIncrement メソッドの実装です。

public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}

getAndAddLong の実装は以下の通りです。

public final long getAndAddLong(Object obj, long offset, long delta) {
    long current;
    do {
        current = getLongVolatile(obj, offset);
    } while (!compareAndSwapLong(obj, offset, current, current + delta));
    return current;
}

この do-while ループが自旋動作そのものです。 CAS による更新に失敗する限り、ループを抜けいず、他のスレッドによる更新を監視し続けることで、再試行を継続します。

再入可能な自旋鎖の実装例

AtomicReference を活用した再入可能な自旋鎖の実装例を以下に示します。

import java.util.concurrent.atomic.AtomicReference;

public class ReentrantSpinLock {
    private final AtomicReference<Thread> owner = new AtomicReference<>();
    private int holdCount = 0;

    public synchronized void lock() {
        Thread current = Thread.currentThread();
        if (owner.get() == current) {
            holdCount++;
            return;
        }
        while (!owner.compareAndSet(null, current)) {
            // 自旋中は NOP または Thread.yield() などを挿入可能
        }
    }

    public synchronized void unlock() {
        Thread current = Thread.currentThread();
        if (owner.get() != current) {
            throw new IllegalMonitorStateException();
        }
        if (--holdCount > 0) {
            return;
        }
        owner.set(null);
    }

    public boolean isHeldByCurrentThread() {
        return owner.get() == Thread.currentThread();
    }
}

この実装では、try-lock 風に treat されるのではなく、明示的な lock/unlock 関係で再入カウントを管理しています。 owner のスレッドが同一であれば、カウントアップのみを行い、ループによる再取得を行いません。

自旋锁の欠点

自旋鎖には明確な欠点があります。 それが長時間ロックが解放されない場合、ループ実行による CPU リソースの無駄reuse( busy-waiting )が発生する点です。 クリティカルセクションの処理時間が不規則または長くなる可能性があれば、むしろブロッキング方式の方が効率的となるケースも少なくありません。

適用すべきシーン

  • クリティカルセクションが非常に短く、処理時間にばらつきがあることが少ない
  • ロック競合が稀に発生する、もしくは保有時間が極めて短い
  • マルチコア環境で CPU 資源を余らせる余裕がある

逆に、長時間ロックを保持するような処理や、高頻度・長時間の競合が予想される場合は、自旋鎖の使用は避けるべきです。

タグ: Spinlock cas Java-Concurrent AtomicReference thread-synchronization

6月18日 18:32 投稿