자바 언어·플랫폼 용어 사전
실무 사례DCL · Double-Checked Locking · 홀더 패턴

이중 검사 잠금

락 비용을 줄이려는 지연 초기화 패턴. volatile 없이 쓰면 재정렬 때문에 조용히 깨진다.

지연 초기화에서 락 비용을 줄이려고 검사를 두 번 하는 패턴. 가장 유명한 동시성 버그의 무대다.

깨지는 코드

private static Singleton instance;            // volatile 이 없다

static Singleton get() {
    if (instance == null) {                   // ① 락 없이 한 번 확인 (빠른 경로)
        synchronized (Singleton.class) {
            if (instance == null)             // ② 락 잡고 다시 확인
                instance = new Singleton();
        }
    }
    return instance;
}

왜 깨지나 — 재정렬

instance = new Singleton() 은 한 단계가 아니다

ⓐ 메모리를 할당한다 ⓑ 생성자를 실행해 필드를 채운다 ⓒ instance 에 그 주소를 대입한다

JIT·CPU 는 ⓑ 와 ⓒ 의 순서를 바꿀 수 있다 (단일 스레드 결과는 같으니까)

ⓐ → ⓒ → ⓑ 로 실행되면

  • 다른 스레드가 ① 에서 "null 이 아니네" 하고 통과한다
  • 생성자가 아직 안 끝난 '반쯤 만들어진 객체' 를 받는다
  • 필드가 기본값(0 · null)인 상태로 쓰인다

재현이 거의 안 되고, 특정 JVM·부하에서만 터진다.

고치는 법

private static volatile Singleton instance;   // ← 이 한 단어

volatileⓑ와 ⓒ의 재정렬을 막고(메모리 배리어) 가시성도 보장한다. Java 5 이후의 메모리 모델에서 이 패턴이 비로소 안전해졌다.

더 나은 방법 — 홀더 패턴

class Singleton {
    private Singleton() { }
    private static class Holder { static final Singleton INSTANCE = new Singleton(); }
    static Singleton get() { return Holder.INSTANCE; }
}

Holder 클래스는 get() 이 처음 불릴 때 초기화된다 (지연 초기화) 클래스 초기화의 스레드 안전성은 JVM 이 보장한다

  • 락도 volatile 도 필요 없다. 코드도 더 짧다

DCL을 직접 쓸 일은 사실상 없다 — 필드 단위 지연 초기화가 정말 필요할 때나 쓴다.

같은 뿌리의 형제 사고

  • 안전하지 않은 발행(unsafe publication)
    • 생성자에서 this 를 다른 스레드에 넘기면 미완성 객체가 노출된다
  • 불변 객체는 이 문제가 없다
    • final 필드는 생성자 완료 시점에 보이는 것이 보장된다 (final field semantics)

면접 함정

  • "DCL은 옛날 JVM 버그" → 메모리 모델상의 재정렬 문제이고, volatile 없이는 지금도 깨진다.
  • "synchronized 안이니 안전하다" → 첫 번째 검사는 락 밖이다. 그게 문제의 지점이다.

다른 지연 초기화 선택지

// ① enum 싱글턴 — 직렬화·리플렉션 공격까지 막아 준다
enum Singleton {
    INSTANCE;
    void work() { ... }
}

// ② Java 25 의 StableValue — 지연 1회 초기화를 언어가 지원한다
private final StableValue<Config> cfg = StableValue.of();
Config cfg() { return cfg.orElseSet(this::loadConfig); }   // 최초 1회만 로드

// ③ 인스턴스 필드라면
private final Supplier<Heavy> lazy = Suppliers.memoize(Heavy::new);   // 라이브러리

②가 나오면서 DCL을 손으로 쓸 이유가 사실상 사라졌다 — 홀더 패턴이 못 다루던 인스턴스 필드 지연 초기화까지 커버한다.

왜 이 사고가 유명한가

  • 코드가 논리적으로 완벽해 보인다

  • 단일 스레드 테스트는 100% 통과한다

  • 개발 환경(적은 부하 · 인터프리터)에서는 재현되지 않는다

  • 운영에서 간헐적으로, 재현 불가능하게 터진다

  • "메모리 모델을 모르면 눈으로 못 잡는 버그" 의 대표 사례로 교과서에 실렸다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 노하우·실제 사고 사례