본문 바로가기

volatile과 AtomicInteger

가시성과 원자성을 분리해 증가 손실을 재현하고, AtomicInteger 연산으로 공유 카운터의 선형화 지점과 확인 항목을 세웁니다.

두 스레드가 같은 int를 천 번씩 증가시키면 최종값이 2,000일 것이라 기대합니다.

그러나 value++는 읽기, 덧셈, 쓰기의 세 단계이며 두 스레드가 같은 이전 값을 읽으면 한 증가가 덮어써집니다.

volatile 쓰기는 같은 필드의 뒤따르는 읽기와 happens-before 관계를 만들지만, 읽기·덧셈·쓰기를 하나의 사건으로 합치지는 않습니다. 여기서 앞뒤는 동기화 순서를 기준으로 합니다.

공유 값이 단순 카운터라면 전체 메서드를 잠그기보다 AtomicInteger가 의도를 더 직접적으로 표현합니다.

원자 클래스의 증가 메서드는 한 연산처럼 관찰되는 선형화 지점을 제공합니다.

다만 카운터와 다른 필드를 함께 바꿔야 한다면 원자 변수 하나로 복합 불변식까지 보호되지는 않습니다.


volatile 증가와 값 손실

다음 코드는 두 작업자가 같은 시작 래치를 통과하게 합니다. 실제로 동시에 실행된다는 보장은 없습니다.

이번 실행은 expected=1000000, actual=686911을 출력했습니다. 손실량은 실행마다 달라질 수 있으며, 다른 실행에서 기대 총량이 나와도 ++의 원자성이 증명되는 것은 아닙니다.

bad/VolatileLostUpdate.java
import java.util.concurrent.CountDownLatch;

public final class VolatileLostUpdate {
    private volatile int value;

    void increment() { value++; }

    public static void main(String[] args) throws InterruptedException {
        VolatileLostUpdate counter = new VolatileLostUpdate();
        CountDownLatch start = new CountDownLatch(1);
        Runnable task = () -> {
            try { start.await(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
            for (int i = 0; i < 500_000; i++) {
                counter.increment();
            }
        };
        Thread a = Thread.ofPlatform().start(task);
        Thread b = Thread.ofPlatform().start(task);
        start.countDown();
        a.join();
        b.join();
        System.out.println("expected=1000000 actual=" + counter.value);
    }
}

두 번의 증가가 1을 남기는 가정 순서

두 읽기 0 뒤 두 쓰기 1로 증가가 유실되는 가능한 끼어듦입니다. 각 스레드의 관측값과 공유 값 0→1→1을 단계로 추적합니다.

두 번의 증가가 1을 남기는 가정 순서

value가0인 상태에서 두 작업자가 각각0을읽고1을쓰는 가능한 끼어듦을 설명합니다. 실제 실행 추적이 아닙니다.

가능한 끼어듦 · 읽기와 쓰기

두 번의 증가가 1을 남기는 가정 순서

두 작업자가 같은 0을 읽고 각각 1을 쓰면 두 증가 뒤에도 값은 1입니다.

두 작업자가 같은 이전 값을 읽는 가능한 순서시간은위에서아래로흐른다. 화살표는읽은값또는쓴값의이동을나타낸다. 공유값0에서T1과T2가각각0을읽고각각1을써서최종1을남긴다. 실제로그아닌가능한순서다.T1공유 값T20 읽음0 읽음1 씀1 씀공유 값: 0 → 1 → 1두 증가 · 최종 값 1
두 작업자가 같은 이전 값을 읽는 가능한 순서시간은위에서아래로흐른다. 화살표는읽은값또는쓴값의이동을나타낸다. 공유값0에서T1과T2가각각0을읽고각각1을써서최종1을남긴다. 실제로그아닌가능한순서다.T1공유 값T20 읽음0 읽음1 씀1 씀공유 값: 0 → 1 → 1두 증가 · 최종 값 1
  1. T1 읽기

    각 작업자의 읽기·쓰기
    0을 읽어 자기 계산에 사용
    그 직후 공유 value
    0
  2. T2 읽기

    각 작업자의 읽기·쓰기
    T1이 쓰기 전에 0을 읽음
    그 직후 공유 value
    0
  3. T1 쓰기

    각 작업자의 읽기·쓰기
    읽었던 0에 1을 더해 기록
    그 직후 공유 value
    1
  4. T2 쓰기

    각 작업자의 읽기·쓰기
    자신도 0에 1을 더해 기록
    그 직후 공유 value
    1 · 두 증가 중 하나가 반영되지 않음

원문은 두 작업자가 각 500,000번 증가한 최종값만 출력합니다. 이 네 단계는 가능한 끼어듦의 예이며, 실제 손실량이나 손실 발생을 고정하지 않습니다.

이 예의 조건

  • 실제 로그가 아닌 가능한 끼어듦 순서입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2시간은 위에서 아래로 흐른다. 화살표는 읽은 값 또는 쓴 값의 이동을 나타낸다. 공유 값 0에서 T1과 T2가 각각 0을 읽고 각각 1을 써서 최종 1을 남긴다. 실제 로그가 아닌 가능한 순서다.
원 도표의 설명과 주석
구분내용
설명시간은 위에서 아래로 흐른다. 화살표는 읽은 값 또는 쓴 값의 이동을 나타낸다. 공유 값 0에서 T1과 T2가 각각 0을 읽고 각각 1을 써서 최종 1을 남긴다. 실제 로그가 아닌 가능한 순서다.
전체 읽기 정보두 번의 증가가 1을 남기는 가정 순서 두 번의 증가가 1을 남기는 가정 순서 value가 0인 상태에서 두 작업자가 각각 0을 읽고 1을 쓰는 가능한 끼어듦을 설명합니다. 실제 실행 추적이 아닙니다. 가능한 끼어듦 · 읽기와 쓰기 두 번의 증가가 1을 남기는 가정 순서 두 작업자가 같은 0을 읽고 각각 1을 쓰면 두 증가 뒤에도 값은 1입니다. T1 읽기 각 작업자의 읽기·쓰기 0을 읽어 자기 계산에 사용 그 직후 공유 value 0 T2 읽기 각 작업자의 읽기·쓰기 T1이 쓰기 전에 0을 읽음 그 직후 공유 value 0 T1 쓰기 각 작업자의 읽기·쓰기 읽었던 0에 1을 더해 기록 그 직후 공유 value 1 T2 쓰기 각 작업자의 읽기·쓰기 자신도 0에 1을 더해 기록 그 직후 공유 value 1 · 두 증가 중 하나가 반영되지 않음 원문은 두 작업자가 각 500,000번 증가한 최종값만 출력합니다. 이 네 단계는 가능한 끼어듦의 예이며, 실제 손실량이나 손실 발생을 고정하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 읽기

T1과 T2가 쓰기 전에 공유 값 0을 각각 읽습니다.

  • T1 관측: 0
  • T2 관측: 0
  • volatile 조회가 증가 전체를 원자 연산으로 만들지 않습니다.
첫 쓰기

T1이 관측값에 1을 더해 씁니다.

  • 공유: 0→1
  • T2 계산: 기존 0+1
  • T2는 T1의 새 값을 다시 읽지 않았습니다.
뒤 쓰기

T2도 1을 씁니다.

  • 공유: 1→1
  • 요청 2번: 최종 1
  • 같은 이전 값의 계산이 증가 하나를 덮어씁니다.

두 번의 증가가 1을 남기는 가정 순서

두 읽기 0 뒤 두 쓰기 1로 증가가 유실되는 가능한 끼어듦입니다. 각 스레드의 관측값과 공유 값 0→1→1을 단계로 추적합니다.

두 번의 증가가 1을 남기는 가정 순서

value가0인 상태에서 두 작업자가 각각0을읽고1을쓰는 가능한 끼어듦을 설명합니다. 실제 실행 추적이 아닙니다.

가능한 끼어듦 · 읽기와 쓰기

두 번의 증가가 1을 남기는 가정 순서

두 작업자가 같은 0을 읽고 각각 1을 쓰면 두 증가 뒤에도 값은 1입니다.

두 작업자가 같은 이전 값을 읽는 가능한 순서시간은위에서아래로흐른다. 화살표는읽은값또는쓴값의이동을나타낸다. 공유값0에서T1과T2가각각0을읽고각각1을써서최종1을남긴다. 실제로그아닌가능한순서다.T1공유 값T20 읽음0 읽음1 씀1 씀공유 값: 0 → 1 → 1두 증가 · 최종 값 1
두 작업자가 같은 이전 값을 읽는 가능한 순서시간은위에서아래로흐른다. 화살표는읽은값또는쓴값의이동을나타낸다. 공유값0에서T1과T2가각각0을읽고각각1을써서최종1을남긴다. 실제로그아닌가능한순서다.T1공유 값T20 읽음0 읽음1 씀1 씀공유 값: 0 → 1 → 1두 증가 · 최종 값 1
  1. T1 읽기

    각 작업자의 읽기·쓰기
    0을 읽어 자기 계산에 사용
    그 직후 공유 value
    0
  2. T2 읽기

    각 작업자의 읽기·쓰기
    T1이 쓰기 전에 0을 읽음
    그 직후 공유 value
    0
  3. T1 쓰기

    각 작업자의 읽기·쓰기
    읽었던 0에 1을 더해 기록
    그 직후 공유 value
    1
  4. T2 쓰기

    각 작업자의 읽기·쓰기
    자신도 0에 1을 더해 기록
    그 직후 공유 value
    1 · 두 증가 중 하나가 반영되지 않음

원문은 두 작업자가 각 500,000번 증가한 최종값만 출력합니다. 이 네 단계는 가능한 끼어듦의 예이며, 실제 손실량이나 손실 발생을 고정하지 않습니다.

이 예의 조건

  • 실제 로그가 아닌 가능한 끼어듦 순서입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2시간은 위에서 아래로 흐른다. 화살표는 읽은 값 또는 쓴 값의 이동을 나타낸다. 공유 값 0에서 T1과 T2가 각각 0을 읽고 각각 1을 써서 최종 1을 남긴다. 실제 로그가 아닌 가능한 순서다.
원 도표의 설명과 주석
구분내용
설명시간은 위에서 아래로 흐른다. 화살표는 읽은 값 또는 쓴 값의 이동을 나타낸다. 공유 값 0에서 T1과 T2가 각각 0을 읽고 각각 1을 써서 최종 1을 남긴다. 실제 로그가 아닌 가능한 순서다.
전체 읽기 정보두 번의 증가가 1을 남기는 가정 순서 두 번의 증가가 1을 남기는 가정 순서 value가 0인 상태에서 두 작업자가 각각 0을 읽고 1을 쓰는 가능한 끼어듦을 설명합니다. 실제 실행 추적이 아닙니다. 가능한 끼어듦 · 읽기와 쓰기 두 번의 증가가 1을 남기는 가정 순서 두 작업자가 같은 0을 읽고 각각 1을 쓰면 두 증가 뒤에도 값은 1입니다. T1 읽기 각 작업자의 읽기·쓰기 0을 읽어 자기 계산에 사용 그 직후 공유 value 0 T2 읽기 각 작업자의 읽기·쓰기 T1이 쓰기 전에 0을 읽음 그 직후 공유 value 0 T1 쓰기 각 작업자의 읽기·쓰기 읽었던 0에 1을 더해 기록 그 직후 공유 value 1 T2 쓰기 각 작업자의 읽기·쓰기 자신도 0에 1을 더해 기록 그 직후 공유 value 1 · 두 증가 중 하나가 반영되지 않음 원문은 두 작업자가 각 500,000번 증가한 최종값만 출력합니다. 이 네 단계는 가능한 끼어듦의 예이며, 실제 손실량이나 손실 발생을 고정하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 읽기

T1과 T2가 쓰기 전에 공유 값 0을 각각 읽습니다.

  • T1 관측: 0
  • T2 관측: 0
  • volatile 조회가 증가 전체를 원자 연산으로 만들지 않습니다.
첫 쓰기

T1이 관측값에 1을 더해 씁니다.

  • 공유: 0→1
  • T2 계산: 기존 0+1
  • T2는 T1의 새 값을 다시 읽지 않았습니다.
뒤 쓰기

T2도 1을 씁니다.

  • 공유: 1→1
  • 요청 2번: 최종 1
  • 같은 이전 값의 계산이 증가 하나를 덮어씁니다.

synchronized increment()도 해결책이지만 단일 수치 연산에는 원자 클래스가 더 좁은 책임을 갖습니다.


공유 상태 요구의 세 측면

  • 가시성은 동기화 관계에 따라 다른 스레드의 쓰기를 어떤 읽기가 관찰할 수 있는지 다룬다.
  • 원자성은 연산의 중간 단계에 다른 참여자가 끼어들 수 없는 하나의 사건을 뜻한다.
  • 순서성은 컴파일러와 CPU 재배치에도 앞뒤 관계가 유지되는지 설명한다.
  • volatile 읽기와 쓰기는 가시성과 happens-before 관계를 제공하지만 복합 증가를 묶지 않는다.
  • AtomicInteger.incrementAndGet은 증가와 결과 반환을 한 원자 연산으로 제공한다.
  • 여러 필드의 합계 같은 불변식에는 공통 락이나 하나의 불변 객체 교체가 필요하다.

원자 변수의 메서드 이름도 결과 형식을 드러냅니다.

getAndIncrement는 이전 값을, incrementAndGet은 증가한 값을 반환합니다.

순번 발급에는 이전 값이 필요한지 새 값이 필요한지 먼저 정해야 경계값 오류를 막습니다.


원자 카운터와 총량 검증

src/AtomicRequestCounter.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public final class AtomicRequestCounter {
    private final AtomicInteger accepted = new AtomicInteger();
    private final AtomicInteger rejected = new AtomicInteger();

    void record(boolean success) {
        if (success) accepted.incrementAndGet();
        else rejected.incrementAndGet();
    }

    int total() { return accepted.get() + rejected.get(); }

    public static void main(String[] args) throws InterruptedException {
        AtomicRequestCounter counter = new AtomicRequestCounter();
        List<Thread> workers = new ArrayList<>();
        for (int worker = 0; worker < 4; worker++) {
            int id = worker;
            workers.add(Thread.ofPlatform().start(() -> {
                for (int i = 0; i < 25_000; i++) {
                    counter.record((i + id) % 5 != 0);
                }
            }));
        }
        for (Thread worker : workers) {
            worker.join();
        }
        System.out.println("accepted=" + counter.accepted.get());
        System.out.println("rejected=" + counter.rejected.get());
        System.out.println("total=" + counter.total());
    }
}

이번 출력은 accepted=80000, rejected=20000, total=100000입니다.

두 원자 변수의 개별 증가는 안전하지만 total()이 둘을 읽는 사이 다른 스레드가 기록할 수 있어 실행 중 스냅샷은 한 순간의 일관된 쌍이 아닐 수 있습니다.

모든 작업 종료 후 읽는 검증에서는 문제가 없고, 실시간 일관된 쌍이 필요하면 하나의 상태 객체를 원자적으로 교체합니다.

예시

실제 쌍이 (accepted=1, rejected=0) → (2, 0) → (2, 1)로 바뀐다고 가정하세요. total()이 첫 단계에서 accepted=1을 읽고 마지막 단계에서 rejected=1을 읽으면 합계는 2입니다. 두 값 각각은 유효한 원자 읽기이지만, 실제로 존재한 적 없는 (1, 1)을 합쳤습니다. AtomicOutcomeSnapshot에서는 Counts 참조 하나를 읽어 그 객체의 두 값을 사용하므로 이런 섞임을 피합니다. 원 main은 join 뒤 읽으므로 이 중간 순서를 강제하거나 관측한 예제는 아닙니다.


복합 상태의 불변 스냅샷 교체

src/AtomicOutcomeSnapshot.java
import java.util.concurrent.atomic.AtomicReference;

public final class AtomicOutcomeSnapshot {
    record Counts(long accepted, long rejected) {
        Counts add(boolean success) {
            return success ? new Counts(accepted + 1, rejected)
                    : new Counts(accepted, rejected + 1);
        }
        long total() { return accepted + rejected; }
    }

    private final AtomicReference<Counts> state =
            new AtomicReference<>(new Counts(0, 0));

    void record(boolean success) {
        state.updateAndGet(current -> current.add(success));
    }

    Counts snapshot() { return state.get(); }

    public static void main(String[] args) throws InterruptedException {
        AtomicOutcomeSnapshot metrics = new AtomicOutcomeSnapshot();
        Thread a = Thread.ofPlatform().start(() -> {
            for (int i = 0; i < 10_000; i++) {
                metrics.record(true);
            }
        });
        Thread b = Thread.ofPlatform().start(() -> {
            for (int i = 0; i < 2_000; i++) {
                metrics.record(false);
            }
        });
        a.join();
        b.join();
        Counts counts = metrics.snapshot();
        System.out.println(counts);
        System.out.println("total=" + counts.total());
    }
}
두 개의 get과 하나의 상태 참조를 구별한다

AtomicRequestCounter의 독립된 두 AtomicInteger 읽기와 AtomicOutcomeSnapshot의 하나의 불변 Counts 참조 읽기를 비교합니다.

원자적 읽기 · 상태의 묶음

두 개의 get과 하나의 상태 참조를 구별한다

두 카운터의 개별 읽기와 불변 Counts 한 개의 읽기는 서로 다른 경계입니다.

AtomicRequestCounter의 total()

원문이 읽는 값
accepted.get() 뒤 rejected.get()
동시 갱신 중의 의미
두 읽기는 별도 연산이므로 한 시점의 쌍을 보장하지 않음

AtomicOutcomeSnapshot의 snapshot()

원문이 읽는 값
state.get()이 반환한 Counts 한 개
동시 갱신 중의 의미
accepted와 rejected가 같은 불변 객체에 들어 있음

두 main은 모든 작업자를 join한 뒤 최종값을 읽습니다. 실행 중 쌍의 불일치나 갱신 함수의 재시도 횟수를 관측하는 코드는 없습니다.

이번 출력은 Counts[accepted=10000, rejected=2000]과 total=12000입니다.

갱신 함수는 충돌 시 여러 번 실행될 수 있으므로 로그나 외부 I/O 같은 부수 효과를 넣으면 안 됩니다.

순수하게 새 값만 계산해야 합니다.


가시성·원자성·불변식에 맞는 도구 선택

요구적합한 도구이유
공유 설정 플래그volatile같은 필드의 읽기·쓰기 동기화
독립 카운터 증가AtomicInteger원자 증가 연산
여러 값 일관 스냅샷AtomicReference불변 상태 전체 교체
큰 임계 영역Lock복수 동작 묶기
경합이 많은 통계 누적LongAdder동시 갱신 중 sum은 원자 스냅샷이 아님

연습 문제

첫 순번이 100이고 최대 102까지만 발급되는 생성기를 작성하세요.

여러 스레드가 호출해도 100, 101, 102가 각각 한 번만 나오고 이후 호출은 빈 결과여야 합니다.

정답과 해설

상한 검사와 증가를 분리하면 두 스레드가 같은 마지막 값을 통과할 수 있습니다.

getAndUpdate가 반환한 이전 값이 범위 안인지 판단합니다. 발급 상한 102를 넘긴 다음 상태는 103으로 고정합니다.

exercise/BoundedSequenceSolution.java
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicInteger;

public final class BoundedSequenceSolution {
    private final AtomicInteger next = new AtomicInteger(100);

    OptionalInt issue() {
        int value = next.getAndUpdate(current -> current <= 102 ? current + 1 : current);
        return value <= 102 ? OptionalInt.of(value) : OptionalInt.empty();
    }

    public static void main(String[] args) throws InterruptedException {
        BoundedSequenceSolution sequence = new BoundedSequenceSolution();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = Thread.ofPlatform().start(() -> System.out.println(sequence.issue()));
        }
        for (Thread thread : threads) {
            thread.join();
        }
    }
}

반환할 이전 값과 다음 상태 103을 나눈다

반환 100/101/102와 다음 상태 101/102/103, 이후 빈 결과·상태 유지입니다. 출력 순서와 분리된 선형화 상태를 같은 카운터로 추적합니다.

반환할 이전 값과 다음 상태 103을 나눈다

getAndUpdate가 반환하는 이전 값으로100부터102를발급하고103에서는빈결과를반환하는 답안의 경계를 보여줍니다.

발급 순서 · 이전 값과 다음 상태

반환할 이전 값과 다음 상태 103을 나눈다

100·101·102는 한 번씩 발급하고, 다음 상태 103에서는 더 발급하지 않습니다.

이전 값 반환과 다음 상태의 분리getAndUpdate에서old100·101·102는각각반환하고next는1증가합니다. old103은next103을유지하며empty를반환합니다. 이는발급상태순서이지println출력순서가아닙니다.현재 next100..103old ≤ 102?예 · 갱신next=old+1아니오next=103상태 유지이전 값 반환100·101·102빈 결과empty
이전 값 반환과 다음 상태의 분리getAndUpdate에서old100·101·102는각각반환하고next는1증가합니다. old103은next103을유지하며empty를반환합니다. 이는발급상태순서이지println출력순서가아닙니다.현재 next100..103old ≤ 102?예 · 갱신next=old+1아니오next=103상태 유지이전 값반환100·101·102빈 결과empty
설명과 원래 예시 값
  1. 이전 값 100

    갱신 뒤 next
    101
    issue()의 반환
    OptionalInt[100]
  2. 이전 값 101

    갱신 뒤 next
    102
    issue()의 반환
    OptionalInt[101]
  3. 이전 값 102

    갱신 뒤 next
    103
    issue()의 반환
    OptionalInt[102]
  4. 이전 값 103

    갱신 뒤 next
    103 · 상태 유지
    issue()의 반환
    OptionalInt.empty

이번 출력은 빈 결과 두 개 뒤 102, 101, 100 순이었습니다. 표는 발급 연산의 상태 순서이며 println의 출력 순서가 아닙니다. 정상 완료한 5회 호출의 결과는 실값 세 개와 빈 결과 두 개입니다.

이 예의 조건

  • 발급 연산 상태 순서이며 println 순서는 아닙니다. 정상 5회 호출입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2getAndUpdate가 반환하는 이전 값으로 100부터 102를 발급하고 103에서는 빈 결과를 반환하는 답안의 경계를 보여줍니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보반환할 이전 값과 다음 상태 103을 나눈다 반환할 이전 값과 다음 상태 103을 나눈다 getAndUpdate가 반환하는 이전 값으로 100부터 102를 발급하고 103에서는 빈 결과를 반환하는 답안의 경계를 보여줍니다. 발급 순서 · 이전 값과 다음 상태 반환할 이전 값과 다음 상태 103을 나눈다 100·101·102는 한 번씩 발급하고, 다음 상태 103에서는 더 발급하지 않습니다. 이전 값 100 갱신 뒤 next 101 issue()의 반환 OptionalInt[100] 이전 값 101 갱신 뒤 next 102 issue()의 반환 OptionalInt[101] 이전 값 102 갱신 뒤 next 103 issue()의 반환 OptionalInt[102] 이전 값 103 갱신 뒤 next 103 · 상태 유지 issue()의 반환 OptionalInt.empty 이번 출력은 빈 결과 두 개 뒤 102, 101, 100 순이었습니다. 표는 발급 연산의 상태 순서이며 println의 출력 순서가 아닙니다. 정상 완료한 5회 호출의 결과는 실값 세 개와 빈 결과 두 개입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 두 발급

getAndUpdate의 이전 값을 반환합니다.

  • 이전 100: next 101, OptionalInt 100
  • 이전 101: next 102, OptionalInt 101
  • 새 next가 아닌 이전 값이 이번 발급값입니다.
마지막 실값

이전 102를 반환하고 next를 103으로 둡니다.

  • 반환: OptionalInt 102
  • 다음 상태: 103
  • 상한 실값은 한 번 발급된 뒤 끝 상태가 됩니다.
빈 결과

이전 103에서 상태를 유지하며 두 빈 결과를 반환합니다.

  • next: 103 유지
  • 5회 결과: 실값 3, 빈 값 2
  • 출력은 빈 값 2 뒤 102,101,100이었지만 발급 상태 순서와 구별합니다.

반환할 이전 값과 다음 상태 103을 나눈다

반환 100/101/102와 다음 상태 101/102/103, 이후 빈 결과·상태 유지입니다. 출력 순서와 분리된 선형화 상태를 같은 카운터로 추적합니다.

반환할 이전 값과 다음 상태 103을 나눈다

getAndUpdate가 반환하는 이전 값으로100부터102를발급하고103에서는빈결과를반환하는 답안의 경계를 보여줍니다.

발급 순서 · 이전 값과 다음 상태

반환할 이전 값과 다음 상태 103을 나눈다

100·101·102는 한 번씩 발급하고, 다음 상태 103에서는 더 발급하지 않습니다.

이전 값 반환과 다음 상태의 분리getAndUpdate에서old100·101·102는각각반환하고next는1증가합니다. old103은next103을유지하며empty를반환합니다. 이는발급상태순서이지println출력순서가아닙니다.현재 next100..103old ≤ 102?예 · 갱신next=old+1아니오next=103상태 유지이전 값 반환100·101·102빈 결과empty
이전 값 반환과 다음 상태의 분리getAndUpdate에서old100·101·102는각각반환하고next는1증가합니다. old103은next103을유지하며empty를반환합니다. 이는발급상태순서이지println출력순서가아닙니다.현재 next100..103old ≤ 102?예 · 갱신next=old+1아니오next=103상태 유지이전 값반환100·101·102빈 결과empty
설명과 원래 예시 값
  1. 이전 값 100

    갱신 뒤 next
    101
    issue()의 반환
    OptionalInt[100]
  2. 이전 값 101

    갱신 뒤 next
    102
    issue()의 반환
    OptionalInt[101]
  3. 이전 값 102

    갱신 뒤 next
    103
    issue()의 반환
    OptionalInt[102]
  4. 이전 값 103

    갱신 뒤 next
    103 · 상태 유지
    issue()의 반환
    OptionalInt.empty

이번 출력은 빈 결과 두 개 뒤 102, 101, 100 순이었습니다. 표는 발급 연산의 상태 순서이며 println의 출력 순서가 아닙니다. 정상 완료한 5회 호출의 결과는 실값 세 개와 빈 결과 두 개입니다.

이 예의 조건

  • 발급 연산 상태 순서이며 println 순서는 아닙니다. 정상 5회 호출입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2getAndUpdate가 반환하는 이전 값으로 100부터 102를 발급하고 103에서는 빈 결과를 반환하는 답안의 경계를 보여줍니다.
원 도표의 설명과 주석
구분내용
설명
전체 읽기 정보반환할 이전 값과 다음 상태 103을 나눈다 반환할 이전 값과 다음 상태 103을 나눈다 getAndUpdate가 반환하는 이전 값으로 100부터 102를 발급하고 103에서는 빈 결과를 반환하는 답안의 경계를 보여줍니다. 발급 순서 · 이전 값과 다음 상태 반환할 이전 값과 다음 상태 103을 나눈다 100·101·102는 한 번씩 발급하고, 다음 상태 103에서는 더 발급하지 않습니다. 이전 값 100 갱신 뒤 next 101 issue()의 반환 OptionalInt[100] 이전 값 101 갱신 뒤 next 102 issue()의 반환 OptionalInt[101] 이전 값 102 갱신 뒤 next 103 issue()의 반환 OptionalInt[102] 이전 값 103 갱신 뒤 next 103 · 상태 유지 issue()의 반환 OptionalInt.empty 이번 출력은 빈 결과 두 개 뒤 102, 101, 100 순이었습니다. 표는 발급 연산의 상태 순서이며 println의 출력 순서가 아닙니다. 정상 완료한 5회 호출의 결과는 실값 세 개와 빈 결과 두 개입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 두 발급

getAndUpdate의 이전 값을 반환합니다.

  • 이전 100: next 101, OptionalInt 100
  • 이전 101: next 102, OptionalInt 101
  • 새 next가 아닌 이전 값이 이번 발급값입니다.
마지막 실값

이전 102를 반환하고 next를 103으로 둡니다.

  • 반환: OptionalInt 102
  • 다음 상태: 103
  • 상한 실값은 한 번 발급된 뒤 끝 상태가 됩니다.
빈 결과

이전 103에서 상태를 유지하며 두 빈 결과를 반환합니다.

  • next: 103 유지
  • 5회 결과: 실값 3, 빈 값 2
  • 출력은 빈 값 2 뒤 102,101,100이었지만 발급 상태 순서와 구별합니다.

갱신 함수는 재실행되어도 외부 효과가 없으므로 CAS 재시도에 안전합니다.


공유 카운터의 최종 설계 기준

volatile을 보았다고 복합 연산이 안전하다고 결론 내리지 않습니다.

공유 작업을 읽기와 쓰기 단계로 펼쳐 다른 스레드가 끼어들 수 있는지 확인합니다.

단일 수치라면 Atomic, 여러 값의 공동 불변식이라면 잠금 또는 불변 스냅샷을 선택합니다.