7. 동기화 - synchronized

임대일·2025년 5월 13일

Thread

목록 보기
7/13
post-thumbnail

1. 공유 자원과 동시성 문제의 시작

멀티스레드에서의 핵심 위험: 동시성 문제

  • 여러 스레드가 공유 자원(예: int balance)에 동시에 접근하면 예상치 못한 오류 발생합니다.
  • 멀티스레드 환경에서는 반드시 공유 자원에 대한 접근 제어(동기화)가 필요합니다.

출금 예제로 보는 동시성 문제

💡 코드 요약

if (balance < amount) {
    return false;
}
balance = balance - amount;
  • 위 코드는 검증 후 출금의 순서로 잔액을 처리
  • 동시에 두 스레드(t1, t2)가 접근하면 둘 다 잔액이 충분하다고 판단해 출금을 진행

문제 상황 요약

시나리오설명
t1, t2가 동시에 출금 시도둘 다 balance = 1000이라고 판단
t1 출금 후 balance = 200t2도 출금해 balance = -600 or 200으로 덮어씀
결과잔액 불일치 또는 마이너스 잔액 발생 (은행 입장에서 재앙)

핵심 원인: 임계 영역(critical section)

  • balance읽고 → 검증하고 → 갱신하는 일련의 작업은 쪼갤 수 없는 단위여야 합니다.
  • 이 로직을 실행하는 동안엔 다른 스레드가 끼어들어선 안 됩니다.
// 임계 영역 예시
if (balance < amount) {
    return false;
}
balance = balance - amount;

정리

  • 멀티스레드 환경에서 가장 중요한 건 공유 자원 보호입니다.
  • 문제를 일으키는 지점은 "임계 영역" → 반드시 한 번에 하나의 스레드만 실행되도록 보호 필요합니다.
  • 자바는 이를 위해 synchronized 키워드를 제공합니다.

2. synchronized로 임계 영역 보호하기

문제 해결: BankAccountV2 - synchronized 메서드 도입

public synchronized boolean withdraw(int amount) {
    // 검증 & 출금
}
  • synchronized 메서드는 해당 메서드 전체를 임계 영역으로 만듭니다.
  • 하나의 스레드만 이 메서드에 동시에 진입 가능 → 락(lock)을 기반으로 동작합니다.

동작 방식

  1. 객체(인스턴스)는 하나의 락(lock)을 가집니다.
  2. synchronized 메서드는 해당 객체의 락을 획득한 스레드만 진입 가능합니다.
  3. 다른 스레드는 BLOCKED 상태가 되어 락이 해제될 때까지 대기합니다.

실행 흐름 예시 (t1, t2 순서로 실행)

시점설명
t1 실행락을 획득하고 withdraw() 진입
t2 실행락이 없어서 BLOCKED 상태
t1 완료락을 해제함
t2락을 획득하고 withdraw() 진입
결과잔액 정확하게 계산됨 (잔액 = 200원)

synchronized자동으로 락을 얻고 반납하므로 직접 unlock할 필요가 없습니다.


락 획득 순서 보장 여부

  • 락을 기다리는 스레드가 많아도 어떤 스레드가 먼저 락을 획득할지는 보장되지 않습니다.
  • 즉, 락 획득 순서는 무작위이며, 특정 스레드가 계속 못 얻을 수도 있습니다. → 공정성 문제

참고: volatile과의 차이

  • volatile가시성 문제(변경된 값을 즉시 반영)만 해결합니다.
  • 동기화 불가: balance를 읽고 쓰는 동안 중간에 다른 스레드가 끼어들 수 있습니다.

따라서 계산 과정 전체를 보호하려면 반드시 synchronized가 필요합니다.

getBalance()도 보호 필요

public synchronized int getBalance() {
    return balance;
}
  • 읽기 메서드도 중간에 값이 변경될 수 있으므로 보호가 필요합니다.
  • 단, 이 메서드는 간단하므로 synchronized 메서드로 충분합니다.

3. synchronized 코드 블럭 – 최소 범위로 임계 영역 지정하기

문제 인식

public synchronized boolean withdraw(int amount) {
    log("거래 시작");
    // ... (임계 영역 시작)
    sleep(1000);
    balance -= amount;
    // ... (임계 영역 끝)
    log("거래 종료");
}
  • 전체 메서드가 synchronized로 잠겨 있으므로, 공유 자원과 관계없는 로그 출력까지도 락 점유에 포함됩니다.
  • 결과적으로 불필요한 대기가 생겨 성능 저하가 발생합니다.

해결 방법: 코드 블럭에만 synchronized 적용

public boolean withdraw(int amount) {
    log("거래 시작");

    synchronized (this) {
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
        if (balance < amount) {
            log("[검증 실패]");
            return false;
        }
        log("[검증 완료]");
        sleep(1000);
        balance -= amount;
        log("[출금 완료]");
    }

    log("거래 종료");
    return true;
}
  • synchronized (this)해당 인스턴스의 락을 명시적으로 지정합니다.
  • 공유 자원을 다루는 부분만 잠금 처리합니다. → 성능 향상

어떤 부분을 보호해야 하나?

  • balance 조회 → 검증 → 차감 이 3단계를 묶는 것이 핵심입니다.
  • 그 외의 로그 출력, 계산되지 않는 부수 작업은 잠금 처리 X

성능 측면에서 유리한 이유

방식장점단점
synchronized 메서드 전체구현이 간단함보호 필요 없는 부분도 락 걸림
synchronized 블럭필요한 부분만 보호 가능코드가 다소 복잡해짐

고성능이 중요하다면 → 블럭 단위로 최소화된 임계 영역 사용 권장합니다.

4. 문제 풀이: 공유 자원과 동기화 실습

문제 1: 공유 자원 없이 동기화 X

public void increment() {
    count = count + 1;
}
  • 멀티스레드 환경에서 count는 공유 자원입니다.
  • count++는 사실상 3단계 연산:
    1. 읽기
    2. 연산
    3. 쓰기
  • 동시에 두 스레드가 접근하면 값 손실 발생

해결: synchronized 키워드 적용

public synchronized void increment() {
    count = count + 1;
}
  • 단일 스레드만 increment()에 진입 가능하려면 synchronized를 추가합니다. → 동시성 문제 해결

문제 2: 지역 변수는 공유 대상인가?

public void count() {
    int localValue = 0;
    for (int i = 0; i < 1000; i++) {
        localValue++;
    }
    log("결과: " + localValue);
}

: 공유되지 않습니다. → 동기화 불필요

  • 지역 변수는 각 스레드의 스택 메모리에 존재합니다.
  • 다른 스레드와 공유되지 않으므로 경합 조건이 없습니다.

문제 3: final 필드는 안전한가?

private final int value;

: 안전합니다.

  • final 필드는 초기화 이후 절대 변경 불가능합니다.
  • 여러 스레드가 읽더라도 동시성 문제가 없습니다.
  • 읽기 전용 자원은 동기화 없이 안전하게 공유가 가능합니다.

전체 정리: synchronized 요약

항목설명
목적공유 자원에 한 번에 하나의 스레드만 접근하게 함
키워드synchronized (메서드, 블럭 단위 모두 가능)
락(lock)객체 단위로 존재, 모니터 락이라 부름
상태락을 못 얻으면 BLOCKED 상태 진입
가시성synchronized메모리 가시성 보장도 포함
성능과도한 동기화는 성능 저하 → 최소 임계 영역 설정 필요

synchronized 단점 (그리고 자바 1.5 이후 해결책)

단점설명
무한 대기락 못 얻으면 BLOCKED 상태에서 끝없이 기다림
공정성 부족어떤 스레드가 락을 얻을지 순서 보장 없음
인터럽트 불가BLOCKED 상태는 interrupted()로도 깰 수 없음

자바 1.5부터 등장한 java.util.concurrent 패키지는 위의 단점을 보완한 고급 동시성 제어 도구들을 제공합니다 (ReentrantLock, Semaphore, 등)

profile
🤔오늘의 복습은❓

0개의 댓글