본문 바로가기

CAS 스핀 락

검사와 변경을 분리한 가짜 락의 경쟁 가능성을 분석하고, CAS 소유권·finally 해제와 스핀 대기의 한계를 코드 및 실행 결과로 구분합니다.

불리언 필드를 보고 비어 있으면 true로 바꾸는 코드는 검사와 변경 사이에 경쟁이 발생할 수 있는 틈이 있습니다.

CAS는 두 동작을 원자적으로 묶어 한 스레드만 false에서 true 전이에 성공하게 합니다.

실패한 스레드는 잠들지 않고 반복하므로 이것을 스핀 락이라고 부릅니다.

스핀은 대기가 매우 짧을 때 문맥 전환 비용을 피할 수 있지만 기다리는 동안 CPU를 계속 사용합니다.

네트워크나 파일 I/O를 임계 영역 안에 넣으면 락을 못 얻은 코어가 유용한 일을 하지 못합니다.

학습용 구현을 범용 Lock처럼 배포해서는 안 됩니다.


volatile 검사와 쓰기가 겹치는 가짜 락

bad/CheckThenSetLock.java
public final class CheckThenSetLock {
    private volatile boolean locked;
    private int entrants;

    void lock() {
        while (locked) {
            Thread.onSpinWait();
        }
        locked = true;
    }
    void unlock() { locked = false; }

    public static void main(String[] args) throws InterruptedException {
        CheckThenSetLock lock = new CheckThenSetLock();
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                lock.lock();
                lock.entrants++;
                lock.unlock();
            }
        };
        Thread a = Thread.ofPlatform().start(task);
        Thread b = Thread.ofPlatform().start(task);
        a.join(); b.join();
        System.out.println("expected=200000 actual=" + lock.entrants);
    }
}

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

false 검사 둘 뒤 true 쓰기 둘이 함께 진입을 허용하는 가능한 끼어듦입니다. 실제 관측 총량과 달리 예시 순서라는 조건을 유지해 분리된 경계를 추적합니다.

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

CheckThenSetLock의 분리된locked읽기와쓰기사이에두호출자가통과할수있는 가능한 순서입니다. 실제진입수측정이아닙니다.

검사와 변경 · 분리된 경계

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

false 검사와 true 쓰기가 나뉘면 두 호출자가 모두 검사를 통과할 수 있습니다.

분리된 두 읽기가 허용하는 중복 진입가능한끼어듦의예에서T1쓰기전에T1과T2가모두false를읽습니다. 각각true를써도두호출자의진입권을하나로제한하지못하며실측진입순서를뜻하지않습니다.공유 locked=falseT1 false 읽기아직 안 씀T2 false 읽기T1 쓰기 전T1 true 쓰기lock 반환T2 true 쓰기lock 반환T1 해제 전에도두 호출자 진입 가능
분리된 두 읽기가 허용하는 중복 진입가능한끼어듦의예에서T1쓰기전에T1과T2가모두false를읽습니다. 각각true를써도두호출자의진입권을하나로제한하지못하며실측진입순서를뜻하지않습니다.공유 locked=falseT1 false읽기아직 안 씀T2 false읽기T1 쓰기 전T1 true쓰기lock 반환T2 true쓰기lock 반환T1 해제 전에도두 호출자 진입 가능
설명과 원래 예시 값
  1. T1의 while 검사

    원문의 동작
    locked가 false라 반복을 벗어남
    직후 locked와 진입 가능성
    false · 아직 true를 쓰지 않음
  2. T2의 while 검사

    원문의 동작
    T1의 쓰기 전에 false를 읽고 벗어남
    직후 locked와 진입 가능성
    false · 두 호출자가 모두 검사를 통과
  3. T1의 쓰기

    원문의 동작
    locked = true 뒤 lock() 반환
    직후 locked와 진입 가능성
    true · T1이 entrants를 증가시킬 수 있음
  4. T2의 쓰기

    원문의 동작
    자신도 locked = true 뒤 lock() 반환
    직후 locked와 진입 가능성
    true · T1이 해제하기 전에 함께 진입 가능

이번 실행은 expected=200000, actual=195755를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.

이 예의 조건

  • 실측 실행 순서가 아닌 두 읽기 뒤 두 쓰기의 가능한 경합입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2CheckThenSetLock의 분리된 locked 읽기와 쓰기 사이에 두 호출자가 통과할 수 있는 가능한 순서입니다. 실제 진입 수 측정이 아닙니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 CheckThenSetLock의 분리된 locked 읽기와 쓰기 사이에 두 호출자가 통과할 수 있는 가능한 순서입니다. 실제 진입 수 측정이 아닙니다. 검사와 변경 · 분리된 경계 두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 false 검사와 true 쓰기가 나뉘면 두 호출자가 모두 검사를 통과할 수 있습니다. T1의 while 검사 원문의 동작 locked가 false라 반복을 벗어남 직후 locked와 진입 가능성 false · 아직 true를 쓰지 않음 T2의 while 검사 원문의 동작 T1의 쓰기 전에 false를 읽고 벗어남 직후 locked와 진입 가능성 false · 두 호출자가 모두 검사를 통과 T1의 쓰기 원문의 동작 locked = true 뒤 lock() 반환 직후 locked와 진입 가능성 true · T1이 entrants를 증가시킬 수 있음 T2의 쓰기 원문의 동작 자신도 locked = true 뒤 lock() 반환 직후 locked와 진입 가능성 true · T1이 해제하기 전에 함께 진입 가능 이번 실행은 expected=200000, actual=195755를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
검사 통과

T1이 false를 읽고 쓰기 전에 T2도 false를 읽습니다.

  • locked: false
  • 통과자: T1,T2 둘
  • 검사와 변경 분리가 두 호출자의 동시 통과를 허용합니다.
첫 변경

T1이 locked=true를 쓰고 lock에서 반환합니다.

  • locked: true
  • T1: entrants 증가 가능
  • 이미 통과한 T2를 이 쓰기로 되돌리지 않습니다.
겹친 진입

T2도 true를 쓰고 T1 해제 전에 진입할 수 있습니다.

  • 가능 상태: 임계 구역 겹침
  • 관측 총량: expected 200000, actual 195755
  • 다른 실행에서 총량이 같아도 상호 배제가 증명되지 않습니다.

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

false 검사 둘 뒤 true 쓰기 둘이 함께 진입을 허용하는 가능한 끼어듦입니다. 실제 관측 총량과 달리 예시 순서라는 조건을 유지해 분리된 경계를 추적합니다.

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

CheckThenSetLock의 분리된locked읽기와쓰기사이에두호출자가통과할수있는 가능한 순서입니다. 실제진입수측정이아닙니다.

검사와 변경 · 분리된 경계

두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정

false 검사와 true 쓰기가 나뉘면 두 호출자가 모두 검사를 통과할 수 있습니다.

분리된 두 읽기가 허용하는 중복 진입가능한끼어듦의예에서T1쓰기전에T1과T2가모두false를읽습니다. 각각true를써도두호출자의진입권을하나로제한하지못하며실측진입순서를뜻하지않습니다.공유 locked=falseT1 false 읽기아직 안 씀T2 false 읽기T1 쓰기 전T1 true 쓰기lock 반환T2 true 쓰기lock 반환T1 해제 전에도두 호출자 진입 가능
분리된 두 읽기가 허용하는 중복 진입가능한끼어듦의예에서T1쓰기전에T1과T2가모두false를읽습니다. 각각true를써도두호출자의진입권을하나로제한하지못하며실측진입순서를뜻하지않습니다.공유 locked=falseT1 false읽기아직 안 씀T2 false읽기T1 쓰기 전T1 true쓰기lock 반환T2 true쓰기lock 반환T1 해제 전에도두 호출자 진입 가능
설명과 원래 예시 값
  1. T1의 while 검사

    원문의 동작
    locked가 false라 반복을 벗어남
    직후 locked와 진입 가능성
    false · 아직 true를 쓰지 않음
  2. T2의 while 검사

    원문의 동작
    T1의 쓰기 전에 false를 읽고 벗어남
    직후 locked와 진입 가능성
    false · 두 호출자가 모두 검사를 통과
  3. T1의 쓰기

    원문의 동작
    locked = true 뒤 lock() 반환
    직후 locked와 진입 가능성
    true · T1이 entrants를 증가시킬 수 있음
  4. T2의 쓰기

    원문의 동작
    자신도 locked = true 뒤 lock() 반환
    직후 locked와 진입 가능성
    true · T1이 해제하기 전에 함께 진입 가능

이번 실행은 expected=200000, actual=195755를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.

이 예의 조건

  • 실측 실행 순서가 아닌 두 읽기 뒤 두 쓰기의 가능한 경합입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2CheckThenSetLock의 분리된 locked 읽기와 쓰기 사이에 두 호출자가 통과할 수 있는 가능한 순서입니다. 실제 진입 수 측정이 아닙니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 CheckThenSetLock의 분리된 locked 읽기와 쓰기 사이에 두 호출자가 통과할 수 있는 가능한 순서입니다. 실제 진입 수 측정이 아닙니다. 검사와 변경 · 분리된 경계 두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정 false 검사와 true 쓰기가 나뉘면 두 호출자가 모두 검사를 통과할 수 있습니다. T1의 while 검사 원문의 동작 locked가 false라 반복을 벗어남 직후 locked와 진입 가능성 false · 아직 true를 쓰지 않음 T2의 while 검사 원문의 동작 T1의 쓰기 전에 false를 읽고 벗어남 직후 locked와 진입 가능성 false · 두 호출자가 모두 검사를 통과 T1의 쓰기 원문의 동작 locked = true 뒤 lock() 반환 직후 locked와 진입 가능성 true · T1이 entrants를 증가시킬 수 있음 T2의 쓰기 원문의 동작 자신도 locked = true 뒤 lock() 반환 직후 locked와 진입 가능성 true · T1이 해제하기 전에 함께 진입 가능 이번 실행은 expected=200000, actual=195755를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
검사 통과

T1이 false를 읽고 쓰기 전에 T2도 false를 읽습니다.

  • locked: false
  • 통과자: T1,T2 둘
  • 검사와 변경 분리가 두 호출자의 동시 통과를 허용합니다.
첫 변경

T1이 locked=true를 쓰고 lock에서 반환합니다.

  • locked: true
  • T1: entrants 증가 가능
  • 이미 통과한 T2를 이 쓰기로 되돌리지 않습니다.
겹친 진입

T2도 true를 쓰고 T1 해제 전에 진입할 수 있습니다.

  • 가능 상태: 임계 구역 겹침
  • 관측 총량: expected 200000, actual 195755
  • 다른 실행에서 총량이 같아도 상호 배제가 증명되지 않습니다.

volatile 읽기·쓰기는 동기화되지만, while의 검사와 뒤의 쓰기가 하나의 연산이 되지는 않습니다.


스핀 락이 지켜야 하는 소유권 규칙

  • 획득은 compareAndSet false true가 성공한 호출자 한 명에게만 주어진다.
  • 실패한 호출자는 공유 상태를 바꾸지 않고 다음 시도를 준비한다.
  • 임계 영역은 예외가 나도 finally에서 해제되어야 한다.
  • 소유자가 아닌 스레드의 unlock을 막으려면 소유자 식별을 별도로 보관한다.
  • 대기가 길어질 가능성이 있으면 park 기반 Lock으로 전환한다.
  • 아래 구현은 같은 소유자의 재진입을 IllegalStateException으로 거부한다. 이런 검사가 없는 비재진입 스핀 락은 자기 자신을 계속 기다릴 수 있다.

소유자 검사를 포함한 학습용 스핀 락

src/OwnedSpinLock.java
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public final class OwnedSpinLock {
    private final AtomicBoolean locked = new AtomicBoolean();
    private final AtomicReference<Thread> owner = new AtomicReference<>();

    void lock() {
        Thread current = Thread.currentThread();
        if (owner.get() == current) throw new IllegalStateException("not reentrant");
        while (!locked.compareAndSet(false, true)) {
            Thread.onSpinWait();
        }
        owner.set(current);
    }

    void unlock() {
        Thread current = Thread.currentThread();
        if (!owner.compareAndSet(current, null)) {
            throw new IllegalMonitorStateException("not owner");
        }
        locked.set(false);
    }

    public static void main(String[] args) throws InterruptedException {
        OwnedSpinLock lock = new OwnedSpinLock();
        int[] counter = {0};
        Runnable task = () -> {
            for (int i = 0; i < 50_000; i++) {
                lock.lock();
                try { counter[0]++; }
                finally { lock.unlock(); }
            }
        };
        Thread a = Thread.ofPlatform().start(task);
        Thread b = Thread.ofPlatform().start(task);
        a.join(); b.join();
        System.out.println("counter=" + counter[0]);
    }
}

소유자 경로의 locked와 owner를 순서대로 읽는다

CAS 획득→owner 기록→owner 제거→locked false 해제의 실제 코드 상태입니다. 두 필드를 원자 스냅샷으로 오해하지 않도록 현재 문장과 소유권을 추적합니다.

소유자 경로의 locked와 owner를 순서대로 읽는다

OwnedSpinLock의획득CAS, owner설정, owner제거, locked해제를서로다른코드지점으로분리합니다.

소유권 · 획득과 해제 순서

소유자 경로의 locked와 owner를 순서대로 읽는다

locked의 CAS로 획득한 뒤 owner를 기록하고, owner를 제거한 뒤 locked를 풉니다.

획득 CAS와 소유자 필드의 별도 변경locked의false→true CAS성공자만owner를기록하고임계영역에들어갑니다. owner를null로지운뒤locked=false로해제하며실패한호출자는반복합니다. 필드쌍은원자스냅샷이아닙니다.locked false→true획득 CAS성공자 경로owner=current임계 영역CAS 실패자소유권 없음다시 시도owner=nulllocked=truelocked=false다음 획득 가능
획득 CAS와 소유자 필드의 별도 변경locked의false→true CAS성공자만owner를기록하고임계영역에들어갑니다. owner를null로지운뒤locked=false로해제하며실패한호출자는반복합니다. 필드쌍은원자스냅샷이아닙니다.locked false→true획득 CAS성공자경로owner=current임계 영역CAS실패자소유권없음다시 시도owner=nulllocked=truelocked=false다음 획득가능
설명과 원래 예시 값
  1. false→true CAS 직후

    locked
    true
    owner
    null · 다음 문장에서 소유자 기록
  2. owner.set(current) 뒤 임계 영역

    locked
    true
    owner
    현재 소유 스레드
  3. finally의 unlock에서 owner 제거 뒤

    locked
    true · 아직 다음 획득 불가
    owner
    null
  4. locked.set(false)로 해제

    locked
    false · 다음 CAS가 획득 가능
    owner
    null

CAS에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.

이 예의 조건

  • 필드 표기는 소유자 코드 지점이며 다른 스레드의 원자 스냅샷이 아닙니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2OwnedSpinLock의 획득 CAS, owner 설정, owner 제거, locked 해제를 서로 다른 코드 지점으로 분리합니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보소유자 경로의 locked와 owner를 순서대로 읽는다 소유자 경로의 locked와 owner를 순서대로 읽는다 OwnedSpinLock의 획득 CAS, owner 설정, owner 제거, locked 해제를 서로 다른 코드 지점으로 분리합니다. 소유권 · 획득과 해제 순서 소유자 경로의 locked와 owner를 순서대로 읽는다 locked의 CAS로 획득한 뒤 owner를 기록하고, owner를 제거한 뒤 locked를 풉니다. false→true CAS 직후 locked true owner null · 다음 문장에서 소유자 기록 owner.set(current) 뒤 임계 영역 locked true owner 현재 소유 스레드 finally의 unlock에서 owner 제거 뒤 locked true · 아직 다음 획득 불가 owner null locked.set(false)로 해제 locked false · 다음 CAS가 획득 가능 owner null CAS에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
CAS 획득

false→true CAS가 성공합니다.

  • locked: true
  • owner: 아직 null
  • 실패한 다른 호출자는 이 소유자 경로에 진입하지 않습니다.
소유자 기록

owner.set(current) 뒤 임계 구역을 수행합니다.

  • locked: true
  • owner: 현재 스레드
  • 소유 기록이 뒤 unlock의 검증 대상이 됩니다.
순서 있는 해제

owner를 지운 뒤 locked=false를 기록합니다.

  • 중간: owner null, locked true
  • 마지막: owner null, locked false
  • owner 제거만으로 다음 CAS 획득을 허용하지 않습니다.

소유자 경로의 locked와 owner를 순서대로 읽는다

CAS 획득→owner 기록→owner 제거→locked false 해제의 실제 코드 상태입니다. 두 필드를 원자 스냅샷으로 오해하지 않도록 현재 문장과 소유권을 추적합니다.

소유자 경로의 locked와 owner를 순서대로 읽는다

OwnedSpinLock의획득CAS, owner설정, owner제거, locked해제를서로다른코드지점으로분리합니다.

소유권 · 획득과 해제 순서

소유자 경로의 locked와 owner를 순서대로 읽는다

locked의 CAS로 획득한 뒤 owner를 기록하고, owner를 제거한 뒤 locked를 풉니다.

획득 CAS와 소유자 필드의 별도 변경locked의false→true CAS성공자만owner를기록하고임계영역에들어갑니다. owner를null로지운뒤locked=false로해제하며실패한호출자는반복합니다. 필드쌍은원자스냅샷이아닙니다.locked false→true획득 CAS성공자 경로owner=current임계 영역CAS 실패자소유권 없음다시 시도owner=nulllocked=truelocked=false다음 획득 가능
획득 CAS와 소유자 필드의 별도 변경locked의false→true CAS성공자만owner를기록하고임계영역에들어갑니다. owner를null로지운뒤locked=false로해제하며실패한호출자는반복합니다. 필드쌍은원자스냅샷이아닙니다.locked false→true획득 CAS성공자경로owner=current임계 영역CAS실패자소유권없음다시 시도owner=nulllocked=truelocked=false다음 획득가능
설명과 원래 예시 값
  1. false→true CAS 직후

    locked
    true
    owner
    null · 다음 문장에서 소유자 기록
  2. owner.set(current) 뒤 임계 영역

    locked
    true
    owner
    현재 소유 스레드
  3. finally의 unlock에서 owner 제거 뒤

    locked
    true · 아직 다음 획득 불가
    owner
    null
  4. locked.set(false)로 해제

    locked
    false · 다음 CAS가 획득 가능
    owner
    null

CAS에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.

이 예의 조건

  • 필드 표기는 소유자 코드 지점이며 다른 스레드의 원자 스냅샷이 아닙니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2OwnedSpinLock의 획득 CAS, owner 설정, owner 제거, locked 해제를 서로 다른 코드 지점으로 분리합니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보소유자 경로의 locked와 owner를 순서대로 읽는다 소유자 경로의 locked와 owner를 순서대로 읽는다 OwnedSpinLock의 획득 CAS, owner 설정, owner 제거, locked 해제를 서로 다른 코드 지점으로 분리합니다. 소유권 · 획득과 해제 순서 소유자 경로의 locked와 owner를 순서대로 읽는다 locked의 CAS로 획득한 뒤 owner를 기록하고, owner를 제거한 뒤 locked를 풉니다. false→true CAS 직후 locked true owner null · 다음 문장에서 소유자 기록 owner.set(current) 뒤 임계 영역 locked true owner 현재 소유 스레드 finally의 unlock에서 owner 제거 뒤 locked true · 아직 다음 획득 불가 owner null locked.set(false)로 해제 locked false · 다음 CAS가 획득 가능 owner null CAS에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
CAS 획득

false→true CAS가 성공합니다.

  • locked: true
  • owner: 아직 null
  • 실패한 다른 호출자는 이 소유자 경로에 진입하지 않습니다.
소유자 기록

owner.set(current) 뒤 임계 구역을 수행합니다.

  • locked: true
  • owner: 현재 스레드
  • 소유 기록이 뒤 unlock의 검증 대상이 됩니다.
순서 있는 해제

owner를 지운 뒤 locked=false를 기록합니다.

  • 중간: owner null, locked true
  • 마지막: owner null, locked false
  • owner 제거만으로 다음 CAS 획득을 허용하지 않습니다.

이번 최종 출력은 counter=100000입니다.

이 구현은 재진입을 IllegalStateException으로 거부하고, 비소유자의 unlock을 IllegalMonitorStateException으로 거부합니다. main은 이 예외 분기를 실행하지 않습니다. 획득 반복에 시간 제한이나 인터럽트 처리는 없으며 공정성·Condition도 제공하지 않습니다.


잠긴 gate에서 실패 횟수를 세는 탐침

src/SpinBudgetProbe.java
import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.LongAdder;

public final class SpinBudgetProbe {
    private final AtomicBoolean gate = new AtomicBoolean();
    private final LongAdder failedAttempts = new LongAdder();

    boolean tryLockUntil(Duration budget) {
        long deadline = System.nanoTime() + budget.toNanos();
        do {
            if (gate.compareAndSet(false, true)) return true;
            failedAttempts.increment();
            Thread.onSpinWait();
        } while (System.nanoTime() < deadline);
        return false;
    }

    void unlock() { gate.set(false); }

    public static void main(String[] args) throws InterruptedException {
        SpinBudgetProbe probe = new SpinBudgetProbe();
        probe.gate.set(true);
        Thread contender = Thread.ofPlatform().start(() ->
                System.out.println("acquired=" + probe.tryLockUntil(Duration.ofMillis(2))));
        contender.join();
        probe.unlock();
        System.out.println("failedAttempts=" + probe.failedAttempts.sum());
    }
}

join이 끝날 때까지 gate를 열지 않는 탐침

join까지 gate=true로 고정돼 CAS가 반복 실패하고 false 반환 뒤에야 해제합니다. 획득 시도·예산 종료·뒤 해제를 순서대로 구분합니다.

join이 끝날 때까지 gate를 열지 않는 탐침

SpinBudgetProbe main의gate=true설정, contender종료회수, 해제순서로획득실패의이유를설명합니다.

탐침 순서 · 유지된 게이트

join이 끝날 때까지 gate를 열지 않는 탐침

main은 경쟁자가 반환하고 join이 끝난 뒤에야 gate를 false로 풉니다.

보유 gate와 join 뒤 해제의 인과main이gate=true를유지하는동안contender의CAS는반복실패합니다. 2ms예산검사뒤false반환·종료가결정되고join이끝난뒤에만main이gate=false를씁니다.시작 전 gate=truemaincontender joincontenderCAS false실패 횟수 증가예산 검사2ms 요청false 반환contender 종료join 반환그 뒤 gate=false
보유 gate와 join 뒤 해제의 인과main이gate=true를유지하는동안contender의CAS는반복실패합니다. 2ms예산검사뒤false반환·종료가결정되고join이끝난뒤에만main이gate=false를씁니다.시작 전 gate=truemaincontenderjoincontenderCAS false실패 횟수증가예산 검사2ms 요청false 반환contender 종료join 반환그 뒤 gate=false
설명과 원래 예시 값
  1. main이 시작 전에 설정

    gate 상태
    true
    경쟁자의 결과 또는 main의 다음 동작
    경쟁자는 잠긴 gate로 시작
  2. 경쟁자의 do-while

    gate 상태
    계속 true
    경쟁자의 결과 또는 main의 다음 동작
    CAS는 실패하고 failedAttempts가 증가
  3. main의 contender.join() 반환 뒤

    gate 상태
    그제야 false로 해제
    경쟁자의 결과 또는 main의 다음 동작
    경쟁자의 false 반환이 이미 결정된 뒤

이번 출력은 acquired=false, failedAttempts=1863입니다. do-while은 예산 검사 전에 적어도 한 번 시도합니다. 실패 횟수는 2ms의 실제 경과 시간이나 CPU 사용량을 뜻하지 않습니다.

이 예의 조건

  • main은 contender join 뒤에만 gate를 해제합니다. 실패 횟수는 시간·CPU 측정이 아닙니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2SpinBudgetProbe main의 gate=true 설정, contender 종료 회수, 해제 순서로 획득 실패의 이유를 설명합니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보join이 끝날 때까지 gate를 열지 않는 탐침 join이 끝날 때까지 gate를 열지 않는 탐침 SpinBudgetProbe main의 gate=true 설정, contender 종료 회수, 해제 순서로 획득 실패의 이유를 설명합니다. 탐침 순서 · 유지된 게이트 join이 끝날 때까지 gate를 열지 않는 탐침 main은 경쟁자가 반환하고 join이 끝난 뒤에야 gate를 false로 풉니다. main이 시작 전에 설정 gate 상태 true 경쟁자의 결과 또는 main의 다음 동작 경쟁자는 잠긴 gate로 시작 경쟁자의 do-while gate 상태 계속 true 경쟁자의 결과 또는 main의 다음 동작 CAS는 실패하고 failedAttempts가 증가 main의 contender.join() 반환 뒤 gate 상태 그제야 false로 해제 경쟁자의 결과 또는 main의 다음 동작 경쟁자의 false 반환이 이미 결정된 뒤 이번 출력은 acquired=false, failedAttempts=1863입니다. do-while은 예산 검사 전에 적어도 한 번 시도합니다. 실패 횟수는 2ms의 실제 경과 시간이나 CPU 사용량을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
보유 조건 고정

main이 시작 전에 gate=true를 설정합니다.

  • gate: true
  • 경쟁자: 잠긴 상태에서 시작
  • 경쟁자 실행 중 main이 gate를 열지 않습니다.
예산 내 시도

do-while이 CAS 실패를 세며 예산을 검사합니다.

  • CAS: 계속 false
  • 관측 failedAttempts: 1863
  • 예산 검사 전에 적어도 한 번 시도합니다.
종료 후 해제

contender가 false를 반환하고 join이 끝난 뒤 false로 풉니다.

  • acquired: false
  • gate 해제: 경쟁자 결과 결정 뒤
  • 해제 시점이 뒤라 이 탐침에서 획득 성공할 경로가 없습니다.

join이 끝날 때까지 gate를 열지 않는 탐침

join까지 gate=true로 고정돼 CAS가 반복 실패하고 false 반환 뒤에야 해제합니다. 획득 시도·예산 종료·뒤 해제를 순서대로 구분합니다.

join이 끝날 때까지 gate를 열지 않는 탐침

SpinBudgetProbe main의gate=true설정, contender종료회수, 해제순서로획득실패의이유를설명합니다.

탐침 순서 · 유지된 게이트

join이 끝날 때까지 gate를 열지 않는 탐침

main은 경쟁자가 반환하고 join이 끝난 뒤에야 gate를 false로 풉니다.

보유 gate와 join 뒤 해제의 인과main이gate=true를유지하는동안contender의CAS는반복실패합니다. 2ms예산검사뒤false반환·종료가결정되고join이끝난뒤에만main이gate=false를씁니다.시작 전 gate=truemaincontender joincontenderCAS false실패 횟수 증가예산 검사2ms 요청false 반환contender 종료join 반환그 뒤 gate=false
보유 gate와 join 뒤 해제의 인과main이gate=true를유지하는동안contender의CAS는반복실패합니다. 2ms예산검사뒤false반환·종료가결정되고join이끝난뒤에만main이gate=false를씁니다.시작 전 gate=truemaincontenderjoincontenderCAS false실패 횟수증가예산 검사2ms 요청false 반환contender 종료join 반환그 뒤 gate=false
설명과 원래 예시 값
  1. main이 시작 전에 설정

    gate 상태
    true
    경쟁자의 결과 또는 main의 다음 동작
    경쟁자는 잠긴 gate로 시작
  2. 경쟁자의 do-while

    gate 상태
    계속 true
    경쟁자의 결과 또는 main의 다음 동작
    CAS는 실패하고 failedAttempts가 증가
  3. main의 contender.join() 반환 뒤

    gate 상태
    그제야 false로 해제
    경쟁자의 결과 또는 main의 다음 동작
    경쟁자의 false 반환이 이미 결정된 뒤

이번 출력은 acquired=false, failedAttempts=1863입니다. do-while은 예산 검사 전에 적어도 한 번 시도합니다. 실패 횟수는 2ms의 실제 경과 시간이나 CPU 사용량을 뜻하지 않습니다.

이 예의 조건

  • main은 contender join 뒤에만 gate를 해제합니다. 실패 횟수는 시간·CPU 측정이 아닙니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2SpinBudgetProbe main의 gate=true 설정, contender 종료 회수, 해제 순서로 획득 실패의 이유를 설명합니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보join이 끝날 때까지 gate를 열지 않는 탐침 join이 끝날 때까지 gate를 열지 않는 탐침 SpinBudgetProbe main의 gate=true 설정, contender 종료 회수, 해제 순서로 획득 실패의 이유를 설명합니다. 탐침 순서 · 유지된 게이트 join이 끝날 때까지 gate를 열지 않는 탐침 main은 경쟁자가 반환하고 join이 끝난 뒤에야 gate를 false로 풉니다. main이 시작 전에 설정 gate 상태 true 경쟁자의 결과 또는 main의 다음 동작 경쟁자는 잠긴 gate로 시작 경쟁자의 do-while gate 상태 계속 true 경쟁자의 결과 또는 main의 다음 동작 CAS는 실패하고 failedAttempts가 증가 main의 contender.join() 반환 뒤 gate 상태 그제야 false로 해제 경쟁자의 결과 또는 main의 다음 동작 경쟁자의 false 반환이 이미 결정된 뒤 이번 출력은 acquired=false, failedAttempts=1863입니다. do-while은 예산 검사 전에 적어도 한 번 시도합니다. 실패 횟수는 2ms의 실제 경과 시간이나 CPU 사용량을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
보유 조건 고정

main이 시작 전에 gate=true를 설정합니다.

  • gate: true
  • 경쟁자: 잠긴 상태에서 시작
  • 경쟁자 실행 중 main이 gate를 열지 않습니다.
예산 내 시도

do-while이 CAS 실패를 세며 예산을 검사합니다.

  • CAS: 계속 false
  • 관측 failedAttempts: 1863
  • 예산 검사 전에 적어도 한 번 시도합니다.
종료 후 해제

contender가 false를 반환하고 join이 끝난 뒤 false로 풉니다.

  • acquired: false
  • gate 해제: 경쟁자 결과 결정 뒤
  • 해제 시점이 뒤라 이 탐침에서 획득 성공할 경로가 없습니다.

2ms가 전체 반환 시간의 상한은 아니며, nanoTime 절대값 비교의 순환 경계까지 처리한 일반 시간 제한 구현도 아닙니다.

실제 잠금에는 ReentrantLock.tryLock을 사용합니다.


대기 길이와 공정성으로 스핀 판단

임계 영역스핀 적합성권장
매우 짧은 계산부하와 경쟁을 측정한 뒤 검토검증된 원자 구조
대기가 길어지는 연산반복 시도 비용 고려ReentrantLock
외부 I/O 포함부적합락 밖 처리
공정성·취소 필요직접 구현 부족표준 Lock

연습 문제

여러 스레드가 동시에 초기화를 요청해도 실제 초기화 본문은 한 번만 실행되어야 합니다.

실패한 호출자는 스핀하지 않고 false를 반환하게 만드세요.

정답과 해설
exercise/InitializeOnceSolution.java
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public final class InitializeOnceSolution {
    private final AtomicBoolean claimed = new AtomicBoolean();
    private final AtomicInteger runs = new AtomicInteger();

    boolean initialize() {
        if (!claimed.compareAndSet(false, true)) return false;
        runs.incrementAndGet();
        return true;
    }

    public static void main(String[] args) throws InterruptedException {
        InitializeOnceSolution service = new InitializeOnceSolution();
        Thread[] threads = new Thread[20];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = Thread.ofPlatform().start(service::initialize);
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("runs=" + service.runs.get());
    }
}

이번 출력은 runs=1입니다. false는 다른 호출자가 초기화 권한을 선점했다는 뜻이며, 그 호출자의 초기화가 끝났다는 신호는 아닙니다. 이 main은 모든 join 뒤에 runs만 출력합니다.

초기화가 실패했을 때 재시도를 허용하려면 상태를 CLAIMED, READY, FAILED로 확장하고 실패 공개 정책을 설계해야 합니다.


스핀 락 실험의 종료 기준

CAS 스핀 락은 원리를 보여 주지만 범용 잠금의 대체품은 아닙니다.

기다림이 짧다는 근거가 없거나 취소와 공정성이 필요하면 표준 Lock을 선택하세요.

직접 CAS를 쓸 때는 실패 경로와 소유권 해제를 먼저 확인합니다.