자바 언어·플랫폼 용어 사전
동시성Compare-And-Swap · AtomicInteger · LongAdder · ABA 문제

CAS

예상값과 같을 때만 바꾸는 원자적 CPU 명령. 락 없이 동시성을 얻지만 고경합에서는 오히려 손해다.

"현재 값이 내가 본 값과 같으면 새 값으로 바꾼다" 를 원자적으로 수행하는 CPU 명령(x86의 CMPXCHG).

어떻게 락 없이 되나

// AtomicInteger.incrementAndGet() 의 개념적 구현
int incrementAndGet() {
    while (true) {
        int cur = get();                       // 현재 값을 읽는다
        if (compareAndSet(cur, cur + 1))       // 그사이 안 바뀌었으면 교체 성공
            return cur + 1;
        // 실패했다면 누가 먼저 바꾼 것 → 다시 읽고 재시도
    }
}
락:  못 들어가면 스레드가 '멈춘다' (블로킹 · 컨텍스트 스위치)
CAS: 실패하면 '다시 시도한다' (논블로킹 · 스핀)

언제 유리하고 언제 불리한가

  • 경합이 낮다 — CAS 가 압도적 — 첫 시도에 성공하고 스레드 전환이 없다
  • 경합이 높다 — 재시도가 폭증한다 → CPU 를 태우며 헛돈다
    • 이때는 차라리 락으로 줄 세우는 편이 낫다

고경합 카운터는 LongAdder

// ❌ 수십 스레드가 동시에 증가시키면 CAS 재시도가 폭증한다
AtomicLong counter = new AtomicLong();

// ✅ 내부적으로 셀을 여러 개 두고 분산 증가시킨 뒤 합산한다
LongAdder counter = new LongAdder();
counter.increment();
long total = counter.sum();      // 합산 시점에만 모은다

정확한 순간 값이 필요 없는 통계·메트릭에는 LongAdder가 정답이다.

ABA 문제

스레드 1: A 를 읽는다 → (잠시 멈춤) → CAS(A → C) 시도 → 성공 스레드 2: 그사이 A → B → A 로 두 번 바꿨다

값은 A 그대로라 CAS 는 성공한다 그런데 '그동안 아무 일도 없었다' 는 전제는 깨졌다

// 스탬프(버전)를 함께 비교해 해결한다
AtomicStampedReference<Node> ref = new AtomicStampedReference<>(node, 0);
ref.compareAndSet(expected, next, expectedStamp, expectedStamp + 1);

단순 카운터에서는 문제가 안 되고, 락프리 자료구조(스택·큐) 를 직접 만들 때 드러난다.

어디에 쓰이나

AtomicInteger · AtomicLong · AtomicReference ConcurrentHashMap 의 버킷 갱신 ReentrantLock 내부의 상태 전이 (AQS) 가상 스레드 스케줄러 · ForkJoinPool 의 작업 훔치기

"락 없는" 자료구조라고 해도 결국 CAS 위에 서 있다.

면접 함정

  • "CAS는 항상 락보다 빠르다" → 고경합에서는 재시도로 CPU를 태운다.
  • "논블로킹이라 대기가 없다" → 스핀하며 CPU를 쓴다. 대기가 없는 게 아니라 형태가 다르다.

하드웨어에서는 무엇인가

  • x86 — CMPXCHG 명령 (LOCK 접두어와 함께)

  • ARM — LDREX / STREX 쌍 (Load-Exclusive / Store-Exclusive)

  • LOCK 접두어는 캐시 라인을 배타적으로 잡는다

    • 다른 코어의 사본을 무효화한다
    • 그래서 경합이 심하면 캐시 핑퐁이 폭증한다

CAS가 고경합에서 느려지는 물리적 이유가 여기 있다. 재시도 루프 자체보다 캐시 라인을 주고받는 비용이 크다.

VarHandle — 저수준 접근

// Java 9+ : Unsafe 없이 CAS 를 직접 쓸 수 있다
private static final VarHandle VH;
static {
    VH = MethodHandles.lookup().findVarHandle(Node.class, "next", Node.class);
}
VH.compareAndSet(node, expected, newValue);
VH.getAndAdd(obj, 1);

스핀을 CPU에 알려 준다

while (!tryAcquire()) {
    Thread.onSpinWait();     // Java 9+ — CPU 에 "스핀 중" 을 힌트로 준다(x86 PAUSE)
}

하이퍼스레딩 환경에서 형제 스레드에 자원을 양보하고 전력도 아낀다. 스핀 루프를 직접 짤 일이 있으면 반드시 넣는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 동시성