CAS 스핀 락
검사와 변경을 분리한 가짜 락의 경쟁 가능성을 분석하고, CAS 소유권·finally 해제와 스핀 대기의 한계를 코드 및 실행 결과로 구분합니다.
불리언 필드를 보고 비어 있으면 true로 바꾸는 코드는 검사와 변경 사이에 경쟁이 발생할 수 있는 틈이 있습니다.
CAS는 두 동작을 원자적으로 묶어 한 스레드만 false에서 true 전이에 성공하게 합니다.
실패한 스레드는 잠들지 않고 반복하므로 이것을 스핀 락이라고 부릅니다.
스핀은 대기가 매우 짧을 때 문맥 전환 비용을 피할 수 있지만 기다리는 동안 CPU를 계속 사용합니다.
네트워크나 파일 I/O를 임계 영역 안에 넣으면 락을 못 얻은 코어가 유용한 일을 하지 못합니다.
학습용 구현을 범용 Lock처럼 배포해서는 안 됩니다.
volatile 검사와 쓰기가 겹치는 가짜 락
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 쓰기 둘이 함께 진입을 허용하는 가능한 끼어듦입니다. 실제 관측 총량과 달리 예시 순서라는 조건을 유지해 분리된 경계를 추적합니다.
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를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.
이 예의 조건
- 실측 실행 순서가 아닌 두 읽기 뒤 두 쓰기의 가능한 경합입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | CheckThenSetLock의 분리된 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를 읽습니다. |
|
|
| 첫 변경 T1이 locked=true를 쓰고 lock에서 반환합니다. |
|
|
| 겹친 진입 T2도 true를 쓰고 T1 해제 전에 진입할 수 있습니다. |
|
|
두 호출자가 while을 지난 뒤 각각 true를 쓰는 가정
false 검사 둘 뒤 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를 출력했습니다. 표의 두 읽기 뒤 두 쓰기는 가능한 끼어듦의 예이며 실측 진입 순서가 아닙니다. 다른 실행에서 총량이 기대와 같아도 상호 배제를 증명하지는 않습니다.
이 예의 조건
- 실측 실행 순서가 아닌 두 읽기 뒤 두 쓰기의 가능한 경합입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | CheckThenSetLock의 분리된 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를 읽습니다. |
|
|
| 첫 변경 T1이 locked=true를 쓰고 lock에서 반환합니다. |
|
|
| 겹친 진입 T2도 true를 쓰고 T1 해제 전에 진입할 수 있습니다. |
|
|
volatile 읽기·쓰기는 동기화되지만, while의 검사와 뒤의 쓰기가 하나의 연산이 되지는 않습니다.
스핀 락이 지켜야 하는 소유권 규칙
- 획득은 compareAndSet
falsetrue가 성공한 호출자 한 명에게만 주어진다. - 실패한 호출자는 공유 상태를 바꾸지 않고 다음 시도를 준비한다.
- 임계 영역은 예외가 나도
finally에서 해제되어야 한다. - 소유자가 아닌 스레드의
unlock을 막으려면 소유자 식별을 별도로 보관한다. - 대기가 길어질 가능성이 있으면
park기반 Lock으로 전환한다. - 아래 구현은 같은 소유자의 재진입을 IllegalStateException으로 거부한다. 이런 검사가 없는 비재진입 스핀 락은 자기 자신을 계속 기다릴 수 있다.
소유자 검사를 포함한 학습용 스핀 락
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 해제의 실제 코드 상태입니다. 두 필드를 원자 스냅샷으로 오해하지 않도록 현재 문장과 소유권을 추적합니다.
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에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.
이 예의 조건
- 필드 표기는 소유자 코드 지점이며 다른 스레드의 원자 스냅샷이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | OwnedSpinLock의 획득 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가 성공합니다. |
|
|
| 소유자 기록 owner.set(current) 뒤 임계 구역을 수행합니다. |
|
|
| 순서 있는 해제 owner를 지운 뒤 locked=false를 기록합니다. |
|
|
소유자 경로의 locked와 owner를 순서대로 읽는다
CAS 획득→owner 기록→owner 제거→locked false 해제의 실제 코드 상태입니다. 두 필드를 원자 스냅샷으로 오해하지 않도록 현재 문장과 소유권을 추적합니다.
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에 실패한 다른 호출자는 이 소유자 경로에 진입하지 않고 반복합니다. 두 필드의 표기는 코드 지점 설명이며, 다른 스레드가 한 번에 읽을 수 있는 원자 스냅샷은 아닙니다.
이 예의 조건
- 필드 표기는 소유자 코드 지점이며 다른 스레드의 원자 스냅샷이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | OwnedSpinLock의 획득 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가 성공합니다. |
|
|
| 소유자 기록 owner.set(current) 뒤 임계 구역을 수행합니다. |
|
|
| 순서 있는 해제 owner를 지운 뒤 locked=false를 기록합니다. |
|
|
이번 최종 출력은 counter=100000입니다.
이 구현은 재진입을 IllegalStateException으로 거부하고, 비소유자의 unlock을 IllegalMonitorStateException으로 거부합니다. main은 이 예외 분기를 실행하지 않습니다. 획득 반복에 시간 제한이나 인터럽트 처리는 없으며 공정성·Condition도 제공하지 않습니다.
잠긴 gate에서 실패 횟수를 세는 탐침
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 반환 뒤에야 해제합니다. 획득 시도·예산 종료·뒤 해제를 순서대로 구분합니다.
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은 contender join 뒤에만 gate를 해제합니다. 실패 횟수는 시간·CPU 측정이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | SpinBudgetProbe 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를 설정합니다. |
|
|
| 예산 내 시도 do-while이 CAS 실패를 세며 예산을 검사합니다. |
|
|
| 종료 후 해제 contender가 false를 반환하고 join이 끝난 뒤 false로 풉니다. |
|
|
join이 끝날 때까지 gate를 열지 않는 탐침
join까지 gate=true로 고정돼 CAS가 반복 실패하고 false 반환 뒤에야 해제합니다. 획득 시도·예산 종료·뒤 해제를 순서대로 구분합니다.
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은 contender join 뒤에만 gate를 해제합니다. 실패 횟수는 시간·CPU 측정이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | SpinBudgetProbe 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를 설정합니다. |
|
|
| 예산 내 시도 do-while이 CAS 실패를 세며 예산을 검사합니다. |
|
|
| 종료 후 해제 contender가 false를 반환하고 join이 끝난 뒤 false로 풉니다. |
|
|
2ms가 전체 반환 시간의 상한은 아니며, nanoTime 절대값 비교의 순환 경계까지 처리한 일반 시간 제한 구현도 아닙니다.
실제 잠금에는 ReentrantLock.tryLock을 사용합니다.
대기 길이와 공정성으로 스핀 판단
| 임계 영역 | 스핀 적합성 | 권장 |
|---|---|---|
| 매우 짧은 계산 | 부하와 경쟁을 측정한 뒤 검토 | 검증된 원자 구조 |
| 대기가 길어지는 연산 | 반복 시도 비용 고려 | ReentrantLock |
| 외부 I/O 포함 | 부적합 | 락 밖 처리 |
| 공정성·취소 필요 | 직접 구현 부족 | 표준 Lock |
연습 문제
여러 스레드가 동시에 초기화를 요청해도 실제 초기화 본문은 한 번만 실행되어야 합니다.
실패한 호출자는 스핀하지 않고 false를 반환하게 만드세요.
정답과 해설
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를 쓸 때는 실패 경로와 소유권 해제를 먼저 확인합니다.