어떤 개념일까?

synchronized vs volatile vs Atomic

세가지 모두 멀티스레드 환경에서 공유 변수를 안전하게 다루기 위한 도구이다. 하지만, 해결하는 문제의 범위가 각각 다르다.

동시성 문제에는 크게 3가지의 보장으로 나눠볼 수 있다.

가시성 Visibility

한 스레드가 바꾼 값을 다른 스레드가 즉시 볼 수 있는가?

원자성 Atomicity

복합 연산 (읽기 → 수정 → 쓰기)이 중간에 끼어들지 않고 한 덩어리로 실행이 되는가?

순서 Ordering

컴파일러의 명령어 재배치로 인해 코드 순서가 뒤바뀌지 않는가?

3가지 도구 차이점

도구가시성원자성순서방식
volatile메모리 배리어
synchronized락(lock), 블로킹
Atomic*CAS, 논블로킹

어떤 문제를 해결하려고 나왔을까? 왜 사용 할까?

근본 원인은 JMM (Java Memory Model)과 CPU 캐시 구조에 있다.

  • 각 스레드(CPU 코어)는 메인 메모리의 값을 자기 캐시에 복사해서 사용한다.
  • 스레드 A가 값을 바꿔도, 스레드 B가 옛날 캐시값을 계속 읽어서 가시성 문제가 생긴다.
  • count++를 실행하여도, 두개의 스레드가 같이 진행하면, 갱신이 유실되는 원자성 문제가 생긴다.
  • 컴파일러/JIT/CPU는 성능을 위해 명령어 순서를 바꾸는데, 멀티 스레드에서는 예상치 못한 순서 문제가 생긴다.

어떻게 동작하나?

volatile

  • 변수 읽기/쓰기 시에 항상 메인 메모리에서 직접 읽고 쓰게 한다. (캐시 우회)
  • 읽기/쓰기 시점에 메모리 베리어 를 삽입해서 재배치를 막는다.
  • 단일 읽기, 단일 쓰기만 원자적이다.
  • i++처럼 읽고 쓰는 복합 연삼은 원자성을 보장하지 못한다.
private volatile boolean running = true; // 플래그 가시성 보장

synchronized

  • 객체마다 가진 모니터 락을 획득해야 임계 영역에 진입한다.
  • 한 번에 하나의 스레드만 들어가므로 원자성을 확보한다. (상호 배제)
  • 락, 획득 시점에 캐시 무효화 → 메인 메모리에서 읽기, 해제 시점에 변경분을 메인 메모리에 flush → 가시성 확보
  • 락을 못잡으면 블로킹(대기) 상태가 된다. 그만큼 비용이 크고 컨텍스트 스위칭이 발생할 수 있다.
public synchronized void increment() { count++; } // 원자성+가시성
// 또는
synchronized (lock) { /* 임계 영역 */ }

Atomic*

  • 내부적으로 CAS (Compare And Swap)라는 CPU 단일 명령어를 사용한다.
  • 원리: 누가 바꿨으면 실패하고 다시 시도
  • 락을 잡지 않으므로 논블로킹 (lock free), 대기 없이 계속 재시도(spin) 한다.
  • 단일 변수에 대한 원자적 연산을 락 없이 해결 하기 때문에 synchronized보다 가볍다.
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 락 없이 원자적 +1

언제 쓰고, 언제 안 쓰나?

volatile

  • 쓸 때:
    • 한 스레드가 쓰고 나머지는 읽기만 하는 상태 플래그 (boolean running, 종료 신호 등)
    • 값을 단순히 읽고 쓰기만 하고 이전 값에 의존하지 않을 때
    • 가벼운 가시성만 필요할 때
  • 안 쓸 때:
    • count++, count = count + n 처럼 이전 값에 의존하는 복합 연산 → 원자성 깨짐
    • 여러 변수를 묶어 하나의 일관된 상태로 다뤄야 할 때

synchronized

  • 쓸 때:
    • 여러 변수여러 단계의 로직을 하나의 원자적 단위로 묶어야 할 때
    • 복잡한 임계 영역, 조건부 갱신, 불변식(invariant) 유지가 필요할 때
  • 안 쓸 때:
    • 단순한 카운터 하나 증가 → Atomic이 더 가벼움
    • 단순 플래그 → volatile로 충분
    • 락 경합이 심해 성능 병목이 우려될 때(단, 대안 설계 필요)

Atomic*

  • 쓸 때:
    • 단일 변수에 대한 카운터, 누적기, 시퀀스 발급 등
    • 락 오버헤드 없이 간단한 원자 연산이 필요할 때
    • 적당한 경합 수준에서 성능이 중요할 때
  • 안 쓸 때:
    • 여러 변수의 일관성을 동시에 지켜야 할 때 (CAS는 단일 변수 기준)
    • 경합이 극심해 재시도가 폭증할 때 → 차라리 락이 나을 수 있음

남에게 설명한다면 어떻게 설명할 것인가?


추가 궁금한 질문들

  • happens-before 관계란 정확히 무엇이고, volatile/synchronized가 각각 어떤 happens-before를 만드는가?
  • CAS의 ABA 문제란? AtomicStampedReference는 이걸 어떻게 푸는가?
  • synchronized 메서드 vs 블록 vs ReentrantLock의 차이는? Lock이 주는 추가 기능(tryLock, 공정성, 인터럽트)은?
  • synchronized의 락 최적화(biased/lightweight/heavyweight lock, lock coarsening)는 JVM이 어떻게 처리하나?
  • LongAdderAtomicLong과 뭐가 다른가? 고경합에서 왜 더 빠른가? (셀 분산)
  • AtomicReference로 여러 필드를 묶어 원자적으로 다루는 패턴(불변 객체 통째 교체)은 어떻게 설계하나?
  • Java 21+ 가상 스레드(Virtual Thread) 환경에서 synchronized 블로킹이 왜 문제(pinning)가 되고, 어떻게 대응하나?
  • 동시성 컬렉션(ConcurrentHashMap 등)은 내부적으로 이 셋 중 무엇을 어떻게 조합해 쓰나?