[Java] synchronized 동작 원리 - 모니터 락, CAS

2026. 5. 12. 15:30·Java

synchronized

`synchronized`는 같은 프로세스(JVM) 안에서 동기화를 보장하는 키워드이다.

 

동기화란, 여러 스레드/프로세스가 공유 자원(같은 객체나 변수)에 접근할 때 데이터를 일관되게 유지하도록 맞추는 것을 의미한다.

더 정확히 말하면, 실행 순서와 메모리 상태를 서로 맞추는 것이다.

 

단일 스레드 환경에서도 동작 자체는 보장되지만, 경쟁 자체가 없기 때문에 사실상 멀티 스레드 환경에서만 의미가 있다.

 

여기서 원자성(Atomicity) 과 Thread safe 의 개념을 구분해야 한다.

원자성 vs Thread safe

원자성

특정 연산이 중간에 끊기지 않고 완전히 실행되거나, 아예 실행되지 않는 성질을 말한다.

// i++는 사실 3단계 연산
int temp = i;     // 1. read
temp = temp + 1;  // 2. increment
i = temp;         // 3. write

synchronized 없이는 스레드 A가 1번을 실행한 직후 스레드 B가 끼어들 수 있다. synchronized는 이 3단계를 하나의 블록으로 묶어 원자적으로 만든다.

 

Thread safe

멀티스레드 환경에서 공유 상태가 항상 일관성 있게 유지되는 성질이다.

 

이 둘을 굳이 구분하는 이유는, 원자성이 보장돼도 Thread safe가 아닐 수 있기 때문이다.

class Counter {
  private int count = 0;

  public synchronized void increment() { count++; } // 원자성 O
  public synchronized void decrement() { count--; } // 원자성 O

  // 문제: check-then-act 패턴
  public void incrementIfZero() {
      if (count == 0) {        // A 스레드가 여기서 0 확인
          // → B 스레드가 끼어들어 increment() 호출
          increment();         // A는 0이 아닌데 그냥 증가시킴
      }
  }
}

increment() 자체는 원자적이지만, incrementIfZero()는 read → check → write가 하나의 원자적 단위로 묶이지 않아서 Thread safe하지 않다.

 

즉, 원자성은 Thread safe를 위한 도구 중 하나일 뿐이다. synchronized로 원자성을 확보해도 설계 수준에서 경쟁 조건(race condition)을 제거해야 진정한 Thread safe가 된다.

 

Thread safe의 3요소
1. 원자성: 작업이 중간에 끊기지 않음 (상호 배제로 달성)
2. 가시성: 변경된 값을 다른 스레드가 즉시 볼 수 있음

3. 순서 보장: 명령어 재정렬 방지 — happens-before 규칙


synchronized가 단일 JVM에서만 동작하는 이유

synchronized는 JVM 힙에 있는 객체의 모니터 락을 기반으로 동작한다.

그런데 JVM 프로세스끼리는 메모리를 공유하지 않는다.

같은 JVM 안의 스레드들은 동일한 객체의 Mark Word를 보기 때문에 서로의 락 상태를 알 수 있지만, 다른 프로세스 간에는 락을 공유할 수 없다.

 

그래서 다중 서버 환경에서는 synchronized만으로는 부족하고, DB Lock, Redis Lock, Zookeeper 같은 외부 분산 락을 사용해야 한다.


모니터 락

모든 Java 객체는 힙 메모리에 존재하고, 객체 헤더(Object Header)에 락 상태를 기록한다. 그 안의 Mark Word에는 "지금 어떤 스레드가 이 객체를 점유 중인가" 가 기록된다.  

평상시 (synchronized 안 쓸 때)

obj(공유 객체)
┌─────────────────────────────┐
│  Mark Word                  │  ← 해시코드, GC 정보 등 저장
│  (모니터 정보 없음)             │     모니터는 아직 생성 안 됨
└─────────────────────────────┘


synchronized 진입 시도 → 경량 락 먼저 시도

 

용어를 정리하면 다음과 같다.

  • 스레드들은 같은 힙의 같은 객체를 참조하기 때문에 Mark Word를 공유할 수 있고, 그래서 동기화가 가능하다.
  • Mark Word: 객체 헤더의 일부로, 락 상태나 ObjectMonitor 포인터를 저장하는 공간
  • 모니터(ObjectMonitor): `owner + Entry Set + Wait Set`을 포함하는 동기화 구조체
  • Mark Word는 무거운 락 진입 시 ObjectMonitor의 주소를 가리킨다.

CAS (Compare-And-Swap)

CAS는 메모리 주소의 값이 예상값과 일치할 때만 새 값으로 교체하는 CPU 단일 명령어이다.

 

CAS(메모리주소, 예상값, 새값)
  → 메모리주소의 현재값 == 예상값  →  현재값을 새값으로 교체, true 반환
  → 메모리주소의 현재값 != 예상값  →  아무것도 안 함, false 반환

 

예시로 보면 더 명확하다.

상황: count = 0, 스레드A와 스레드B가 동시에 count를 1로 바꾸려 함

스레드A 실행
CAS(count주소, 0, 1)
→ count 현재값(0) == 예상값(0)  → count를 1로 교체, true 반환
→ 락 획득 성공

스레드B 실행 (동시에 시도)
CAS(count주소, 0, 1)
→ count 현재값(1) != 예상값(0)  → 아무것도 안 함, false 반환
→ 락 획득 실패 → Entry Set 대기

 

 "읽기 → 비교 → 쓰기" 3단계를 CPU의 단일 명령어(x86: CMPXCHG) 로 처리하므로 원자성이 보장된다.


경량 락 (Lightweight Lock)

경량 락은 락 획득 시 스레드 스택 프레임에 Lock Record를 생성하고, CAS로 Mark Word에 그 주소를 기록하는 방식이다.

ObjectMonitor를 생성하지 않기 때문에 무거운 락보다 비용이 적다.

Lock Record: 경량 락 획득 시 스레드 스택 프레임에 생성되는 구조체. 원래 Mark Word 값을 백업해두는 공간이자, 락 획득의 증거 역할을 한다.

 

경량 락에서 CAS 동작

공유 객체의 Mark Word 값을 내 스레드 스택의 Lock Record에 복사한다. 이후 그 복사한 값이 Mark Word 주소의 현재 값과 같다면, Lock Record 주소로 교체하여 락을 획득한다.

[락 획득]
CAS(MarkWord주소, 현재MarkWord값, Lock Record주소)

1단계: MarkWord 읽기
 temp = *(MarkWord주소)  → 현재 들어있는 값을 읽어옴

2단계: Lock Record에 백업
 Lock Record.displaced = temp  → 읽어온 값을 백업

3단계: CAS 시도
 CAS(MarkWord주소, temp, Lock Record주소)
 → MarkWord 안의 값이 아직 temp랑 같으면 → 교체 성공
 → 다른 스레드가 이미 바꿔놨으면 → 실패

[락 해제]
CAS(MarkWord주소, Lock Record주소, displaced MarkWord)
→ 백업해둔 원래 MarkWord 값 복원
→ Lock Record 소멸

무거운 락 (Heavyweight Lock)

경량 락 CAS가 실패하면, JVM이 ObjectMonitor를 생성하고 Mark Word를 그 주소로 교체한다. 이 과정을 팽창(inflate)이라고 부른다.

ObjectMonitor는 HotSpot JVM 내부(C++)에 정의된 구조체이다.

무거운 락에서 CAS 동작

[1단계: 팽창]

Mark Word를 ObjectMonitor 포인터로 교체

 

[2단계: 락 획득]

CAS(ObjectMonitor.owner, null, 현재스레드)
→ 성공: owner가 현재 스레드로 세팅 → 락 획득
→ 실패: Entry Set에 들어가서 BLOCK 상태로 대기

synchronized 진입 순간 (무거운 락)

obj(공유 객체)
┌─────────────────────────────┐
│  Mark Word                  │  ← ObjectMonitor 포인터로 교체됨
│  → ObjectMonitor 주소 가리킴   │
└─────────────────────────────┘
         ↓
  ObjectMonitor (JVM이 그때 생성)
┌──────────────────────────────┐
│ owner                        │  ← 현재 락을 점유한 스레드 포인터
│ (스레드A 포인터 저장)             │
├──────────────────────────────┤
│ Entry Set                    │  ← 락 얻으려고 대기 중인 스레드들
├──────────────────────────────┤
│ Wait Set                     │  ← wait() 호출한 스레드들
└──────────────────────────────┘

여기서 owner 필드가 곧 뮤텍스(상호 배제) 역할을 한다. 한 번에 한 스레드만 owner가 될 수 있고, 나머지는 Entry Set에서 대기한다.


정리

  • synchronized는 단일 JVM 내에서의 동기화를 보장하는 키워드다.
  • 원자성과 Thread safe는 다른 개념이며, 원자성은 Thread safe의 한 요소일 뿐이다.
  • JVM은 비용이 적은 경량 락을 먼저 시도하고, 경합이 발생하면 무거운 락으로 팽창시킨다.
  • 두 락 모두 CAS를 통해 원자적으로 락 상태를 변경한다.
  • 다중 서버 환경에서는 synchronized로는 부족하므로 분산 락을 도입해야 한다.

'Java' 카테고리의 다른 글

[Java] Arrays.copyOf가 얕은 복사인 이유 — 어셈블리까지 내려가기  (0) 2026.05.19
[Java] 다형성, 컴포지션, DI - 세 개념을 한 줄로 잇기  (0) 2026.05.17
[Java] enum - 왜 안전한가, private 생성자로 강등되는 이유  (0) 2026.05.14
[Java] equals() & hashCode() 깊게 보기 - 31의 의미, String 내부 동작  (2) 2026.05.12
'Java' 카테고리의 다른 글
  • [Java] Arrays.copyOf가 얕은 복사인 이유 — 어셈블리까지 내려가기
  • [Java] 다형성, 컴포지션, DI - 세 개념을 한 줄로 잇기
  • [Java] enum - 왜 안전한가, private 생성자로 강등되는 이유
  • [Java] equals() & hashCode() 깊게 보기 - 31의 의미, String 내부 동작
oniwon
oniwon
  • oniwon
    눙눙의 개발일지
    oniwon
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Java
      • Spring
      • CS
      • Infra
      • Projects
      • F-Lab
      • Airflow
  • 인기 글

  • 블로그 메뉴

    • 홈
    • 개인정보처리방침 (Privacy Policy)
    • 블로그 소개 (About)
  • 최근 글

  • 태그

    https
    자바면접
    비동기 처리
    자바 내부 동작
    Flab 추천인코드
    inconcert
    Airflow
    Flab 3주차 후기
    Java
    SSE
    에프랩멘토링
    Flab후기
    tls
    Flab커넥터스
    동시성
    Flab 멘토링
    redis
    Flab멘토링
    네트워크
    FLAB
    자바백엔드
    병렬 처리
    F-Lab
    Flab추천인코드
    JVM
    자바 다형성
    에프랩
    dns
    커넥터스2385
    DAG
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.1
oniwon
[Java] synchronized 동작 원리 - 모니터 락, CAS
상단으로

티스토리툴바