본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev21

synchronized 대신 ReentrantLock을 쓰는 이유

  • #Concurrency
  • #Engineering Note
  • #Java

문제 발생

여러 스레드가 공유 자원에 접근하는 코드를 synchronized로 보호했는데, 락을 오래 쥔 스레드 하나 때문에 다른 스레드들이 무한정 대기하는 상황이 발생했고, 대기 중인 요청을 일정 시간 뒤 포기시킬 방법이 없었습니다.

public synchronized void process() {
    // 오래 걸리는 작업
}

원인 분석

synchronized 키워드는 JVM이 관리하는 암묵적 락(모니터 락)을 사용합니다. 간결하고 실수(락 해제를 깜빡하는 것)가 구조적으로 불가능하다는 장점이 있지만, 다음과 같은 제어를 할 수 없습니다.

  • 타임아웃: 락을 얻으려고 무한정 기다리는 것 외의 선택지가 없습니다.
  • 인터럽트 가능 대기: 락을 기다리는 스레드를 중간에 인터럽트로 깨울 수 없습니다.
  • 공정성(fairness): 오래 기다린 스레드가 먼저 락을 얻도록 보장할 방법이 없습니다 — 특정 스레드가 계속 락을 못 얻는 기아(starvation) 상황이 생길 수 있습니다.
  • 락 상태 확인: 지금 이 락이 잠겨 있는지, 몇 개의 스레드가 대기 중인지 등을 조회할 수 없습니다.

해결 방안

java.util.concurrent.locks.ReentrantLock은 명시적인 락 객체로, 위 제어들을 지원합니다.

private final ReentrantLock lock = new ReentrantLock();

public void process() {
    if (lock.tryLock(3, TimeUnit.SECONDS)) {  // 타임아웃 지정 가능
        try {
            // 작업
        } finally {
            lock.unlock();  // 반드시 명시적으로 해제해야 함
        }
    } else {
        // 락을 못 얻었을 때의 대체 로직
    }
}
  1. ReentrantLock은 락 해제를 코드가 직접 책임집니다synchronized와 달리 unlock()을 빠뜨리면 데드락으로 이어질 수 있으므로, 항상 try/finally로 감싸는 것이 필수입니다. synchronized가 자동으로 보장해주던 안전성을 잃는 대가입니다.
  2. 타임아웃, 인터럽트 가능 대기, 공정성 정책이 실제로 필요한 경우에만 ReentrantLock을 씁니다 — 그런 요구가 없는 단순한 상호 배제라면 synchronized가 더 간결하고 실수할 여지가 적습니다.
  3. 읽기가 훨씬 많고 쓰기가 드문 상황이라면 ReentrantReadWriteLock으로 읽기 작업끼리는 동시 진행을 허용해 처리량을 높일 수 있습니다.
  4. Java 21의 가상 스레드(virtual thread) 환경에서는 synchronized 블록 안에서 블로킹 작업을 하면 가상 스레드가 캐리어 스레드를 물고(pin) 놓지 않는 문제가 있어, 이런 상황에서는 ReentrantLock이 더 권장되는 경우도 있습니다 — 최신 JDK 릴리스 노트에서 이 개선 상황을 확인하는 것이 좋습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.