본문으로 건너뛰기
김신건의 로그

[Java] AtomicInteger

· 수정 · 📖 약 2분 · 768자/단어 #java #concurrent #atomic #cas #lock-free
AtomicInteger, java.util.concurrent.atomic.AtomicInteger, 원자 정수, atomic int

정의

java.util.concurrent.atomic.AtomicIntegerlock-free 로 원자성을 보장 하는 int 래퍼. 내부적으로 CAS (Compare-And-Swap) 를 사용해 락 없이 atomic 한 read-modify-write 를 수행.

volatile int i; i++; 가 thread-safe 가 아닌 이유 (read + increment + write 세 단계) 를 해결한다. JDK 1.5 (Java SE 5.0) 에 java.util.concurrent.atomic 패키지와 함께 도입.

사용 상황

상황예시
공유 카운터요청 수, 오류 수 등 멀티스레드 집계
시퀀스 생성unique ID, 트랜잭션 번호
상태 전이단일 int 로 표현 가능한 상태 머신
락 없는 flag초기화 완료 여부 (0/1)

단, 여러 변수 간의 복합 불변식 (예: 잔액 + 거래 이력의 일관성) 은 AtomicInteger 하나로 보장 불가. 이때는 ReentrantLock 또는 synchronized.

시각화

핵심 메서드

AtomicInteger a = new AtomicInteger(0);

// 기본 읽기/쓰기
a.get();                           // 현재 값 (volatile read)
a.set(10);                         // 값 설정 (volatile write)
a.lazySet(10);                     // 비동기 설정 (정렬 보장 없음, GC용)

// 원자 증감
a.incrementAndGet();               // ++a, atomic
a.getAndIncrement();               // a++, 증가 전 값 반환
a.decrementAndGet();               // --a
a.getAndDecrement();               // a--, 감소 전 값 반환
a.addAndGet(5);                    // a += 5, 새 값 반환
a.getAndAdd(5);                    // a += 5, 이전 값 반환

// CAS
a.compareAndSet(10, 99);           // 현재 10 이면 99 로, 성공 여부 반환
// Java 9+
a.compareAndExchange(10, 99);      // 현재 값 반환 (성공하면 old, 실패하면 current)

// Java 8+: 람다 기반
a.updateAndGet(x -> x * 2);       // f(현재값) 으로 갱신, 새 값 반환
a.getAndUpdate(x -> x * 2);       // f(현재값) 으로 갱신, 이전 값 반환
a.accumulateAndGet(3, Integer::sum);   // 누산기 함수 적용
a.getAndAccumulate(3, Integer::sum);

CAS 원리

public final int incrementAndGet() {
    while (true) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))   // current 와 일치하면 next 로
            return next;
        // 일치하지 않으면 다른 스레드가 바꾼 것, 재시도
    }
}

CPU 명령어 (LOCK CMPXCHG on x86) 한 번으로 “비교 후 일치하면 교체” 를 atomic 하게 수행. 락 acquire/release 비용보다 훨씬 작다.

CAS 루프 흐름

flowchart TD
    Start["incrementAndGet() 호출"] --> Read["current = get()"]
    Read --> Calc["next = current + 1"]
    Calc --> CAS{"CAS(current, next) 성공?"}
    CAS -->|"예, current 와 일치"| Done["next 반환"]
    CAS -->|"아니오, 다른 스레드가 변경"| Read
    Other["다른 스레드가\nCAS 먼저 성공"] -.->|"메모리 값 변경"| CAS

경합이 적을 때는 한 번에 성공. 경합이 많을수록 재시도 루프가 길어진다.

volatile vs synchronized vs AtomicInteger

옵션가시성atomic read-modify-write락 비용
int i;없음
volatile int i;✗ (i++ 안 됨)없음
synchronized중간
AtomicInteger✓ (CAS)매우 작음 (저경합)

IMPORTANT

단일 변수의 atomic 갱신만 필요하면 AtomicInteger 가 거의 항상 최선. 여러 변수 간 invariant 보호가 필요하면 ReentrantLock 또는 synchronized.

고경합에서의 문제: LongAdder

CAS 는 저경합에서 빠르다. 하지만 수십 스레드가 같은 AtomicInteger 에 동시 증가 하면 CAS 실패가 폭주, 성능이 락보다 떨어질 수 있다.

이때 LongAdder 가 해법. 내부적으로 여러 Cell 에 분산 누적 후 sum() 으로 합산. 최종 값이 필요한 시점에만 집계하는 카운터에 최적이다.

// 고경합 카운터: LongAdder 권장
LongAdder counter = new LongAdder();
counter.increment();         // 분산 Cell 에 누적
long total = counter.sum();  // 필요할 때 합산

// vs AtomicLong: 고경합 시 CAS 폭주
AtomicLong atomicCounter = new AtomicLong();
atomicCounter.incrementAndGet();

사용 예

카운터

class RequestCounter {
    private final AtomicInteger total = new AtomicInteger();
    private final AtomicInteger errors = new AtomicInteger();

    public void onRequest()  { total.incrementAndGet(); }
    public void onError()    { errors.incrementAndGet(); }
    public int count()       { return total.get(); }
    public int errorCount()  { return errors.get(); }
    public double errorRate() {
        int t = total.get();
        return t == 0 ? 0.0 : (double) errors.get() / t;
    }
}

시퀀스 생성

// Java 17+
class IdGenerator {
    private final AtomicInteger seq = new AtomicInteger(0);

    public int nextId() {
        return seq.incrementAndGet();
    }
}

상태 전이 (CAS)

// Java 17+ 상태 머신 예
sealed interface State permits State.Ready, State.Running, State.Done {}

class Task {
    private static final int READY   = 0;
    private static final int RUNNING = 1;
    private static final int DONE    = 2;

    private final AtomicInteger state = new AtomicInteger(READY);

    public boolean tryStart() {
        return state.compareAndSet(READY, RUNNING);
    }

    public boolean tryFinish() {
        return state.compareAndSet(RUNNING, DONE);
    }

    public boolean isDone() {
        return state.get() == DONE;
    }
}

compareAndSet 으로 상태 전이를 atomic 하게 수행. 두 스레드가 동시에 tryStart() 를 호출해도 하나만 성공.

Java 9+ compareAndExchange

// compareAndSet: boolean 반환
boolean updated = a.compareAndSet(expected, newVal);

// compareAndExchange: 현재 실제 값 반환 (loop-free CAS 에 편리)
int witness = a.compareAndExchange(expected, newVal);
if (witness == expected) {
    // 성공
} else {
    // 실패, witness 가 실제 현재 값
}

ABA 문제

CAS 의 알려진 한계. A → B → A 로 변한 값을 “안 바뀜” 으로 잘못 판단할 수 있다.

// 1. T1 이 값 A 를 읽음
int v = a.get();   // v = A
// 2. T2 가 A -> B -> A 로 변경
// 3. T1 의 CAS(A, x) 성공, 하지만 이 사이에 두 번 바뀐 사실 모름

정수 단순 증감 에서는 ABA 가 문제 되지 않는다. ABA 는 포인터 기반 자료구조 (Lock-free linked list 등) 에서 위험. 해법: AtomicStampedReference 가 stamp (버전) 도 같이 비교.

AtomicStampedReference<String> ref =
    new AtomicStampedReference<>("A", 0);

int[] stampHolder = new int[1];
String val = ref.get(stampHolder);
int stamp = stampHolder[0];

// stamp 도 함께 비교
ref.compareAndSet(val, "B", stamp, stamp + 1);

다른 Atomic 타입

타입용도
AtomicIntegerint 원자 갱신
AtomicLonglong
AtomicBooleanboolean
AtomicReference임의 객체 참조
AtomicIntegerArrayint 배열 원소 원자 갱신
AtomicLongArraylong 배열
AtomicReferenceArray객체 배열
LongAdder고경합 누적 카운터
DoubleAdderdouble 누적
LongAccumulator람다 기반 누산기

함정

1. compound 연산은 atomic 하지 않다

AtomicInteger a = new AtomicInteger(0);

// 두 연산 사이에 다른 스레드 개입 가능
if (a.get() < 10) {
    a.incrementAndGet();   // get 과 increment 사이에 다른 스레드가 변경 가능
}

// 올바름: updateAndGet 으로 원자적으로
a.updateAndGet(v -> v < 10 ? v + 1 : v);

2. int 오버플로 시 조용히 음수로 전환

AtomicInteger max = new AtomicInteger(Integer.MAX_VALUE);
max.incrementAndGet();   // Integer.MIN_VALUE 로 wrap-around

long 이 필요하면 AtomicLong, 또는 오버플로 불가라면 guard 조건 추가.

3. lazySet 은 가시성 보장 없음

a.lazySet(1);   // volatile write 가 아님, 다른 스레드가 보지 못할 수 있음

보통 GC 용도 (null 로 레퍼런스 해제) 나 단일 Writer 특수 케이스에만 사용.

관련 위키

이 글의 용어 (4개)
[Java] AtomicReferencejava
정의 는 객체 참조를 atomic 하게 갱신 하는 래퍼. 참조 + CAS (Compare-And-Swap) 의 결합. 의 객체 버전. 불변 객체 패턴, 상태 머신, lock-fr…
[Java] LongAdderjava
정의 는 고경합 카운터 를 위한 자료구조. JDK 1.8 도입. 보다 동시 증가 throughput 이 압도적으로 높다. 원리는 striped counter (분산 누적). 여러…
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] volatilejava
정의 은 Java 의 키워드. 필드에 붙이면 두 가지를 보장한다. 1. 가시성 (visibility): 한 스레드의 쓰기가 다른 모든 스레드에 즉시 보인다. CPU 캐시에 머무르…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기