운영체제 용어 사전
동시성가시성 · memory barrier · 재정렬

메모리 배리어

명령 재정렬을 막아 다른 코어가 변경을 보게 만드는 장치. 원자성과는 다른 문제다.

명령 재정렬을 막고 변경을 다른 코어가 보도록 강제하는 장치.

원자성과 다른 문제다

  • 원자성 — 연산이 쪼개지지 않는가 (counter++ 가 통째로 실행되는가)

  • 가시성 — 내가 쓴 값을 다른 코어가 보는가

  • 둘은 별개다 — 원자적이어도 안 보일 수 있고, 보여도 원자적이지 않을 수 있다

왜 안 보이나

  • 각 코어가 자기 캐시에 쓴다 (아직 다른 코어에 전파되지 않음)
  • 스토어 버퍼에 쌓여 있다
  • 컴파일러가 레지스터에 올려 두고 메모리에 안 쓴다
  • CPU 가 명령 순서를 바꿔 실행한다 (out-of-order)

단일 스레드에서는 결과가 같아 보이므로 이 최적화가 허용된다 문제는 다른 스레드가 볼 때다

배리어가 하는 일

"이 지점 이전의 메모리 연산이 이후 연산보다 먼저 보이게 하라"

종류

  • LoadLoad · StoreStore · LoadStore · StoreLoad
  • StoreLoad 가 가장 비싸다 (스토어 버퍼를 비워야 한다)

언어 수준에서

volatile int flag;   // 자바: 읽기·쓰기에 배리어가 삽입된다
                     // 가시성 + 재정렬 금지. 원자성은 보장하지 않는다

synchronized         // 진입·해제 시 배리어. 상호배제 + 가시성
  • volatile 로는 counter++ 를 못 고친다 (읽기·증가·쓰기 세 단계)
  • AtomicInteger 나 락이 필요하다

이중 검사 잠금이 깨지는 이유

  • instance = new Singleton() 은

    • ① 메모리 할당 ② 생성자 실행 ③ 참조 대입
  • ② 와 ③이 재정렬되면 다른 스레드가

  • 'null 은 아니지만 생성자가 안 끝난' 객체를 본다

  • volatile 이 재정렬을 막아 해결한다

캐시 일관성이 있는데 왜 안 보이나

흔한 오해: "MESI 로 캐시가 일관되니 가시성 문제는 없다"

캐시는 일관적이다. 문제는 그 앞단이다
  스토어 버퍼   쓴 값이 캐시에 도달하기 전에 잠시 쌓인다
  무효화 큐     무효화 메시지를 나중에 처리한다

즉 '캐시에 들어간 뒤' 는 일관되지만 '캐시에 들어가기 전' 이 비동기다

  • 배리어는 이 버퍼·큐를 비우게 만든다

아키텍처별 강도

  • x86(TSO) — 비교적 강하다. StoreLoad 재정렬만 허용

    • 잘못된 코드가 x86 에서만 우연히 동작하는 일이 흔하다
  • ARM — 훨씬 약하다. 대부분의 재정렬이 허용된다

    • 모바일·서버 ARM 으로 옮기면 잠재 버그가 드러난다
  • "내 머신에서는 되던데" 가 동시성에서 특히 위험한 이유

자바 메모리 모델의 happens-before

락 해제 → 이후 같은 락의 획득 volatile 쓰기 → 이후 같은 변수의 volatile 읽기 스레드 시작 전 동작 → 그 스레드 안의 모든 동작 스레드의 모든 동작 → 다른 스레드의 join() 반환

이 관계가 성립하면 앞의 쓰기가 뒤에서 반드시 보인다 "어느 하드웨어에서든" 을 언어가 보장하는 방식이다

final 필드의 특별 규칙

생성자에서 대입된 final 필드는, 생성자가 끝난 뒤 그 객체 참조를 본 스레드에게 동기화 없이도 올바른 값으로 보인다. 불변 객체를 안전하게 공유할 수 있는 근거이고, 이중 검사 잠금 문제와 짝을 이루는 규칙이다.

면접 함정

  • "volatile이 동기화를 대신한다" → 가시성만이다. 원자성은 별개다.
  • "단일 코어면 문제없다" → 컴파일러 재정렬은 여전히 일어난다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 동시성 — 경쟁 조건·뮤텍스/세마포어·CAS