ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

AtomicReference

 

정의

AtomicReference란 무엇인가

AtomicReference<T>는 객체 참조(reference)를 원자적(atomic)으로 읽고/쓰고/교체하기 위한 클래스입니다.
핵심은 락(synchronized) 없이도 CAS(Compare-And-Set)라는 CPU 원자 연산을 이용해 “현재 값이 예상한 값이면 새 값으로 바꾸기”를 안전하게 수행한다는 점입니다.
핵심 문장: “참조 값을 CAS로 교체하면서 동시성 경쟁을 제어한다”
패키지: java.util.concurrent.atomic.AtomicReference
핵심 메시지
AtomicReference는 “데이터를 조금씩 수정”하는 도구라기보다, 불변(immutable) 객체를 통째로 교체하거나 상태 머신을 참조 교체로 모델링할 때 가장 강력합니다.

 

동작 원리

CAS(Compare-And-Set)로 락 없이 경쟁을 해결한다

CAS 한 줄 요약

compareAndSet(expected, update)는 “현재 값이 expected와 같으면 update로 교체하고 true, 아니면 false”를 원자적으로 수행합니다. 그래서 멀티스레드에서 값이 바뀌는 순간을 정확히 잡아 교체할 수 있습니다.
패턴: 읽기(get) → 계산 → CAS 시도 → 실패 시 재시도(spin)

메모리 가시성(Visibility) 감각

AtomicReference의 get/set/CAS는 멀티스레드 환경에서 “값이 보인다/안 보인다” 문제를 피하도록 volatile 수준의 가시성을 제공합니다. (즉, 한 스레드가 set한 참조는 다른 스레드에서 get으로 관측 가능합니다.)
API 의미 비고
get / set 참조 읽기/쓰기 가시성 보장(volatile 감각)
compareAndSet 예상값이면 교체 락 없이 경쟁 제어
updateAndGet 함수로 갱신 후 반환 내부적으로 CAS 루프

 

언제 쓰나

AtomicReference가 잘 맞는 4가지 시나리오

1) 불변 객체 스냅샷 교체

설정(config), 라우팅 테이블, 캐시 스냅샷 등 “객체를 통째로 바꾸는” 모델

2) 상태 머신(State Machine)

(INIT → RUNNING → STOPPED) 같은 전이를 CAS로 강제해 “중복 실행”을 방지

3) 락을 피하고 싶은 경량 경쟁 구간

짧은 갱신 구간에서 synchronized보다 빠른 경우가 많음(단, 경쟁이 심하면 반대)

4) “한 번만” 초기화(Lazy init) 가드

동시에 여러 스레드가 초기화하려 할 때, CAS로 “승자 1명만” 만들기
GOOD
불변 객체 + 참조 교체
원자적으로 “스냅샷”을 교체하면 읽기/쓰기 경계가 명확해집니다.
BAD
가변 객체를 공유
AtomicReference는 “참조”만 원자적입니다. 객체 내부 필드를 동시에 안전하게 바꿔주지 않습니다.

 

구현

Java 예제로 보는 AtomicReference 패턴

1) 가장 기본: get/set/compareAndSet

import java.util.concurrent.atomic.AtomicReference;

public class AtomicRefBasic {

    public static void main(String[] args) {
        AtomicReference<String> ref = new AtomicReference<>("A");

        String v1 = ref.get();          // "A"
        ref.set("B");                   // B로 교체

        boolean ok = ref.compareAndSet("B", "C"); // 현재가 B면 C로
        System.out.println(ok);         // true
        System.out.println(ref.get());  // "C"
    }
}

2) 정석 패턴: CAS 루프 (락-프리 갱신)

“현재값 기반으로 새 값을 만들고 교체”할 때는 실패 가능성이 있으므로 보통 CAS 루프를 사용합니다.
import java.util.concurrent.atomic.AtomicReference;

public class AtomicRefCasLoop {

    static class State {
        final int count; // 불변
        State(int count) { this.count = count; }
        State inc() { return new State(count + 1); }
    }

    public static void main(String[] args) {
        AtomicReference<State> ref = new AtomicReference<>(new State(0));

        for (;;) {
            State cur = ref.get();
            State next = cur.inc();
            if (ref.compareAndSet(cur, next)) break; // 성공할 때까지 재시도
        }

        System.out.println(ref.get().count); // 1
    }
}

3) 더 깔끔하게: updateAndGet (내부적으로 CAS 루프)

import java.util.concurrent.atomic.AtomicReference;

public class AtomicRefUpdateAndGet {

    static class Config {
        final String mode;
        final int timeoutMs;
        Config(String mode, int timeoutMs) {
            this.mode = mode;
            this.timeoutMs = timeoutMs;
        }
        Config withTimeout(int ms) { return new Config(mode, ms); }
    }

    public static void main(String[] args) {
        AtomicReference<Config> cfg = new AtomicReference<>(new Config("prod", 1000));

        Config newCfg = cfg.updateAndGet(old -> old.withTimeout(1500));
        System.out.println(newCfg.timeoutMs); // 1500
    }
}

 

주의사항

AtomicReference를 “정말 안전하게” 쓰기 위한 함정 3가지

1) AtomicReference는 “참조”만 원자적이다

ref가 가리키는 객체가 가변(mutable)이고, 여러 스레드가 그 내부 필드를 동시에 변경하면 AtomicReference로는 보호되지 않습니다. 원칙: 불변 객체를 만들고, 변경은 새 객체를 생성해 ref를 교체하는 쪽이 안전합니다.

2) ABA 문제(advanced)

값이 A였다가 B로 바뀌었다가 다시 A로 돌아오면, CAS는 “여전히 A네?”라고 착각하고 성공할 수 있습니다. 락-프리 자료구조(스택/큐)에서 특히 문제가 됩니다. 이 위험이 있는 설계라면 AtomicStampedReference(버전 스탬프)나 AtomicMarkableReference(마킹) 같은 변형을 고려합니다.

3) 경쟁이 심하면 “스핀 비용”이 커질 수 있다

CAS는 실패할 수 있고, 실패하면 재시도합니다. 경쟁이 매우 심하면 재시도 루프가 뜨거워져 오히려 synchronized/Lock보다 성능이 나빠질 수 있습니다. 완전한 정답은 없고, 경쟁 정도/작업 단위/코어 수에 따라 선택이 달라집니다.

 

요약

체크리스트로 마무리

CHECK
✅ AtomicReference는 java.util.concurrent.atomic 패키지에 있다
✅ 핵심은 CAS(compareAndSet)로 참조를 원자 교체하는 것
✅ 베스트 프랙티스: 불변 객체를 만들어 통째로 교체
✅ 주의: 가변 객체 내부 변경은 AtomicReference로 보호되지 않음
✅ 고급 이슈: ABA 가능성(필요 시 Stamped/Markable 고려)

 

728x90