Java 메모리 모델과 happens-before

한 줄 정의

Java Memory Model은 여러 스레드가 공유 변수를 읽고 쓸 때 값의 가시성, 실행 순서, 동기화 보장을 정의하는 규칙이다.

실무에서 왜 중요한가

멀티스레드 환경에서는 코드 순서대로 실행되는 것처럼 보여도 다른 스레드가 같은 값을 즉시 본다는 보장이 없다. JMM을 모르면 다음 문제가 생긴다.

  • 한 스레드가 바꾼 값을 다른 스레드가 계속 보지 못한다.
  • volatile을 쓰면 모든 동시성 문제가 해결된다고 오해한다.
  • count++ 같은 연산이 원자적이라고 생각한다.
  • double-checked locking을 잘못 구현한다.
  • 동시성 버그가 재현되지 않아 테스트만 통과하고 운영에서 간헐적으로 실패한다.

Visibility와 Atomicity

visibility는 한 스레드의 변경이 다른 스레드에 보이는지의 문제다.

class StopFlag {
    private boolean running = true;
 
    void stop() {
        running = false;
    }
 
    void run() {
        while (running) {
            // work
        }
    }
}

running 변경이 다른 스레드에 즉시 보인다는 보장이 없다. 이런 경우 volatile을 사용할 수 있다.

private volatile boolean running = true;

하지만 volatile은 복합 연산의 atomicity를 보장하지 않는다.

volatile int count = 0;
 
void increment() {
    count++; // read, add, write 세 단계
}

카운터처럼 원자성이 필요하면 AtomicInteger, LongAdder, synchronized, lock을 검토한다.

happens-before

happens-before는 한 작업의 결과가 다른 작업에 보이도록 보장하는 관계다.

대표적인 예시는 다음과 같다.

관계의미
같은 스레드 내 앞선 작업뒤 작업에서 앞 작업 결과를 볼 수 있다
volatile write 후 readread한 스레드는 write 이전 변경을 볼 수 있다
synchronized unlock 후 lock같은 monitor를 잡은 스레드가 변경을 볼 수 있다
thread start시작 전 설정한 값을 새 thread가 볼 수 있다
thread join종료한 thread의 결과를 join한 thread가 볼 수 있다

JMM은 동시성 코드를 작성할 때 “언제 다른 스레드가 이 값을 볼 수 있는가”를 판단하는 기준이다.

synchronized와 volatile

구분volatilesynchronized
visibility보장보장
atomicity단일 read/write 중심임계 영역 전체 보장
lock사용하지 않음monitor lock 사용
용도상태 플래그, 단순 publish복합 상태 변경, 불변식 보호

volatile은 상태 플래그처럼 단순한 공유 값에 적합하다. 여러 값을 함께 변경하거나 read-modify-write가 필요하면 lock이나 atomic class가 필요하다.

자주 나는 실수

  • volatilecount++를 안전하게 만들 수 있다고 생각한다.
  • 동기화 없이 mutable 객체를 여러 스레드가 공유한다.
  • HashMap을 여러 스레드가 동시에 수정한다.
  • lazy initialization을 동기화 없이 구현한다.
  • 테스트에서 재현되지 않는다고 동시성 문제가 없다고 판단한다.

실무 판단 기준

상황우선 고려
stop flagvolatile boolean
단순 카운터AtomicLong, LongAdder
여러 필드 불변식 보호synchronized 또는 lock
읽기 위주 공유 데이터불변 객체, copy-on-write
요청 단위 상태공유하지 않고 지역 변수로 유지

확인 방법

  • 코드 리뷰에서 static mutable state와 singleton mutable field를 찾는다.
  • 공유 변수에 read-modify-write 연산이 있는지 확인한다.
  • 동시성 테스트는 CountDownLatch로 시작 시점을 맞춘다.
  • thread dump와 메트릭으로 lock contention이나 blocked thread를 확인한다.

핵심 요약

Java Memory Model은 여러 스레드가 공유 데이터를 볼 때 visibility와 실행 순서를 정의하는 규칙입니다.

volatile은 값 변경의 가시성을 보장하지만 count++ 같은 복합 연산의 원자성은 보장하지 않습니다.

synchronized는 같은 monitor를 기준으로 visibility와 임계 영역의 atomicity를 함께 제공합니다.

happens-before 관계를 이해해야 한 스레드의 변경이 다른 스레드에 언제 보이는지 설명할 수 있습니다.

실무에서는 공유 상태를 줄이고, 필요한 경우 atomic class, lock, 불변 객체를 명확한 기준으로 선택해야 합니다.

꼬리 질문

점검 퀴즈

아래 문항은 개념을 실제로 설명할 수 있는지 점검하기 위한 것이다. 선택지를 누르면 정답 여부와 이유가 표시된다.

객관식visibility와 atomicity의 차이로 가장 적절한 것은?

OXvolatile int count에 count++을 하면 멀티스레드에서도 안전한 카운터가 된다.

객관식synchronized가 제공하는 보장으로 가장 적절한 것은?

관련 문서