JAVA

[TIL] Java 멀티스레드 - synchronized와 임계 영역 (2026.05.04)

bang-dev 2026. 5. 4. 21:42

동시성 문제란?

여러 스레드가 같은 공유 자원(인스턴스 필드 등)에 동시에 접근할 때 발생한다.

잔액 1000원 계좌에 t1, t2가 동시에 800원 출금을 시도하는 예제에서 두 가지 문제가 나타났다.

케이스 1 - t1이 약간 먼저 실행

  • t1이 잔액 검증(1000 >= 800) 통과 후 아직 balance를 줄이지 않은 상태
  • t2도 잔액 1000으로 검증 통과
  • 결과: t1 출금 후 잔액 200, t2 출금 후 잔액 -600 (마이너스 발생)

케이스 2 - t1, t2 완전히 동시 실행

  • 둘 다 balance=1000 읽음 → 둘 다 1000-800=200 계산 → 둘 다 balance=200 저장
  • 결과: 1600원이 빠져나갔는데 잔액은 200원 (800원 증발)

volatile로는 해결 안 된다. 메모리 가시성 문제만 해결할 뿐, 원자적 실행을 보장하지 않는다.


임계 영역 (Critical Section)

여러 스레드가 동시에 접근하면 안 되는 공유 자원을 다루는 코드 구간.

출금 예제에서는 잔액 검증 ~ 잔액 차감 전체가 임계 영역이다. 이 구간은 한 번에 하나의 스레드만 실행해야 한다.


synchronized 메서드

public synchronized boolean withdraw(int amount) { ... }
public synchronized int getBalance() { ... }

동작 원리 (모니터 락)

  • 모든 객체는 내부에 락(lock) 을 하나씩 가진다 (모니터 락)
  • synchronized 메서드 진입 시 해당 인스턴스의 락이 필요하다
  • 락이 없으면 → BLOCKED 상태로 무한 대기 (CPU 스케줄링 제외)
  • 락 반납 시 → 대기 중인 스레드 중 하나가 자동으로 락 획득 → RUNNABLE 전환
t1: 락 획득 → withdraw() 실행 → 락 반납
t2: BLOCKED 대기 → 락 획득 → withdraw() 실행 (잔액 200, 검증 실패 → 출금 실패)

결과: t1 800원 출금 성공 / t2 잔액 부족으로 실패 / 최종 잔액 200원

락을 획득하는 순서는 보장되지 않는다.

synchronized 블록 안에서 접근하는 변수는 volatile 없이도 메모리 가시성이 보장된다.


synchronized 코드

메서드 전체가 아닌 꼭 필요한 구간만 동기화할 수 있다.

public boolean withdraw(int amount) {
    log("거래 시작");         // 공유 자원 없음 → 동시 실행 OK

    synchronized (this) {   // 임계 영역만 보호
        if (balance < amount) return false;
        sleep(1000);
        balance = balance - amount;
    }

    log("거래 종료");         // 공유 자원 없음 → 동시 실행 OK
    return true;
}
  • synchronized(this) → this 인스턴스의 락 사용 (메서드 방식과 동일한 락)
  • 임계 영역은 최소 범위로 유지해야 전체 처리 성능이 높아진다

synchronized 장단점

장점

  • 언어 문법으로 제공 → 사용이 간편하다
  • 블록/메서드 종료 시 자동으로 락 해제

단점

  • 무한 대기: BLOCKED 상태에서 타임아웃 없이 락 풀릴 때까지 기다린다
  • 공정성 없음: 어떤 스레드가 다음에 락을 얻는지 보장 안 됨

이 한계 때문에 Java 1.5부터 java.util.concurrent 패키지가 추가됐다.


추가로 정리한 것

지역 변수는 동기화 불필요 — 스택에 생성되어 스레드 간 공유되지 않는다. synchronized 붙여봤자 성능만 떨어진다.

final 필드는 안전 — 아무도 값을 변경할 수 없으므로 멀티스레드 환경에서도 문제없다.

count = count + 1 같은 단순 연산도 위험 — 읽기 → 계산 → 쓰기 3단계로 나뉘어 경쟁 조건이 생긴다.