synchronized 대신 ReentrantLock을 쓰는 이유
문제 발생
여러 스레드가 공유 자원에 접근하는 코드를 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 {
// 락을 못 얻었을 때의 대체 로직
}
}ReentrantLock은 락 해제를 코드가 직접 책임집니다 —synchronized와 달리unlock()을 빠뜨리면 데드락으로 이어질 수 있으므로, 항상try/finally로 감싸는 것이 필수입니다.synchronized가 자동으로 보장해주던 안전성을 잃는 대가입니다.- 타임아웃, 인터럽트 가능 대기, 공정성 정책이 실제로 필요한 경우에만
ReentrantLock을 씁니다 — 그런 요구가 없는 단순한 상호 배제라면synchronized가 더 간결하고 실수할 여지가 적습니다. - 읽기가 훨씬 많고 쓰기가 드문 상황이라면
ReentrantReadWriteLock으로 읽기 작업끼리는 동시 진행을 허용해 처리량을 높일 수 있습니다. - Java 21의 가상 스레드(virtual thread) 환경에서는
synchronized블록 안에서 블로킹 작업을 하면 가상 스레드가 캐리어 스레드를 물고(pin) 놓지 않는 문제가 있어, 이런 상황에서는ReentrantLock이 더 권장되는 경우도 있습니다 — 최신 JDK 릴리스 노트에서 이 개선 상황을 확인하는 것이 좋습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.