synchronized

허세진·2026년 1월 26일

backend

목록 보기
10/20

synchronized

synchronized는 Java에서 공유 자원에 대한 동시 접근을 제어하기 위한 동기화 메커니즘이다.

synchronized가 해결하려는 문제

1. Race Condition

count++;

위 코드는 count를 읽고 1을 더하고 다시 사용하는 코드다.

→ 여러 스레드가 동시에 실행하면 값이 깨진다.

2. 메모리 가시성 문제

while (!flag) { }

한 스레드가 flag = true로 바꿔도 다른 스레드가 CPU 캐시 때문에 변경을 못 볼 수 있다.

synchronized가 보장하는 두 가지 핵심

1. 상호 배제

상호배제는 여러 스레드가 동시에 같은 자원에 접근하지 못하도록 막는 것을 의미한다.

하나의 스레드가 특정 코드나 데이터를 사용하고 있을 때 다른 스레드는 그 영역에 들어올 수 없게 함으로써, 연산 중간에 값이 꼬이거나 데이터가 깨지는 경쟁 상태를 방지한다.

2. 가시성 보장

가시성 보장은 한 스레드가 변경한 값이 다른 스레드에서도 즉시 보이도록 보장하는 것을 의미한다.

CPU 캐시나 컴파일러 최적화로 인해 변경된 값이 다른 스레드에 보이지 않는 문제가 발생할 수 있는데, synchronized는 락을 획득하고 해제하는 과정에서 변경된 값을 메인 메모리에 반영하고 다시 읽게 해서 이런 문제를 해결한다.

synchronized의 동작 원리 (JVM 내부 관점)

ex)
스레드 A, 스레드 B가 있는데 둘 다 아래의 같은 코드를 실행하려고 한다.

synchronized(obj) {
    // 임계 구역
}

스레드는 저 코드를 만나면 obj의 Monitor가 비어 있는지 확인한다.
(Monitor = 객체에 딸린 출입 열쇠)

Monitor가 비어 있는 경우

(스레드 A가 먼저 도착했다고 가정)

먼저 JVM이 obj의 Monitor 상태를 확인한다. 근데 A가 B보다 먼저왔으니 아무도 안 쓰고 있을거니까 스레드 A가 Monitor를 획득한다. 그리고 스레드 A가 임계 구역을 실행한다.

(obj의 Monitor → 스레드 A가 소유 중)

Monitor가 이미 사용 중인 경우

이때 스레드 B가 도착하면 JVM이 obj의 Monitor를 확인하는데, 이미 스레드 A가 가지고 있으니까 스레드 B는 바로 멈춘다. 그대로 CPU를 사용하지 않고 대기한다.

스레드 A가 synchronized 블록을 빠져나올 때

스레드 A가 obj의 Monitor를 반납하고 변경된 값들을 메인 메모리에 반영한다. 그리고 대기 중인 스레드 하나를 깨운다.

이제 스레드 B는 Monitor가 비었음을 확인하고 Monitor 획득해서 실행한다.

객체 헤더와 Mark Word

모든 Java 객체는 메모리에 객체 헤더를 가지고 있고, 그 안에 Mark Word라는 영역이 있다.

Mark Word는 다음 정보들을 저장한다.

  • 해시코드
  • GC 정보
  • 락 정보 (어떤 스레드가 락을 소유하고 있는지)
  • 락 상태 (Unlocked, Biased, Thin Locked, Fat Locked)

synchronized는 이 Mark Word를 활용하여 락을 구현한다.

synchronized 사용 방식 3가지

1. 인스턴스 메서드 동기화

public synchronized void increase() {
    count++;
}

인스턴스 메서드 동기화는 메서드 호출 시 해당 객체(this)의 모니터 락을 획득해서 실행하는 방식이다.

같은 객체의 synchronized 인스턴스 메서드들은 하나의 락을 공유하므로 동시에 실행될 수 없지만, 서로 다른 객체의 메서드는 각각 다른 락을 사용하므로 병렬 실행이 가능하다.

2. static 메서드 동기화

public static synchronized void increase() {
    count++;
}

static 메서드 동기화는 인스턴스가 아니라 클래스 자체의 모니터 락을 획득하여 실행하는 방식이다.

클래스 객체는 JVM 내에 하나만 존재하므로, 어떤 인스턴스에서 호출하더라도 해당 클래스의 static synchronized 메서드는 전체 애플리케이션에서 동시에 하나만 실행된다.

3. 블록 동기화

synchronized(lockObject) {
    count++;
}

블록 동기화는 synchronized 블록에 지정한 특정 객체의 모니터 락을 획득하는 방식으로, 개발자가 직접 락 대상을 선택할 수 있다.

필요한 코드 영역만 동기화할 수 있어 불필요한 락 범위를 줄일 수 있고, 성능과 설계 측면에서 가장 유연하고 권장되는 방식이다.

synchronized의 중요한 특성

재진입 가능성

재진입 가능이란 같은 스레드가 이미 획득한 락을 다시 획득할 수 있다는 의미다.

public synchronized void outer() {
    System.out.println("outer");
    inner();  // 같은 락을 다시 획득 시도
}

public synchronized void inner() {
    System.out.println("inner");  // 정상 실행됨 (데드락 X)
}

JVM은 락을 획득한 스레드를 기록하고, 재진입 횟수를 카운트한다.

  • outer() 진입 → 락 카운트 1
  • inner() 진입 → 락 카운트 2 (같은 스레드이므로 허용)
  • inner() 종료 → 락 카운트 1
  • outer() 종료 → 락 카운트 0 (완전히 해제)

만약 재진입이 불가능하다면 자기 자신이 가진 락을 기다리는 자기 데드락이 발생한다.

예외 발생 시 락 해제 보장

synchronized는 예외가 발생해도 자동으로 락을 해제한다.

synchronized(obj) {
    // 작업 수행
    throw new RuntimeException("에러!");
    // 이 코드는 실행 안 됨
} // 예외가 발생해도 여기서 자동으로 락 해제됨

이는 try-finally 구조가 내부적으로 보장되기 때문이다. 따라서 예외 때문에 락이 영구적으로 잠기는 일은 없다.

synchronized의 한계

1. 공정성 제어 불가

synchronized는 락을 해제할 때 어떤 스레드가 먼저 실행될지 보장하지 않고, 오래 기다린 스레드가 계속 밀리는 기아 현상이 발생할 수 있다.

2. 인터럽트 대응 불가

락을 기다리는 동안 스레드는 BLOCKED 상태가 되고, 이 상태에서는 interrupt()를 호출해도 즉시 깨어나지 못하고 락이 풀릴 때까지 대기한다.

3. tryLock 불가

synchronized는 락 획득에 실패했을 때 대안 동작을 선택할 수 없고, 락을 얻을 때까지 무조건 기다려야 한다.

profile
로그를 파고드는 시간을 즐기는 백엔드 개발자, 허세진입니다.

0개의 댓글