ReentrantLock과 공정성
기본 비공정 설정과 재진입 횟수를 실행하고 `synchronized`보다 넓은 Lock 제어 기능의 비용을 판단합니다.
Lock 인터페이스는 획득과 해제를 메서드로 분리하고, 대표 구현인 ReentrantLock은 중단 가능 대기, 즉시·시간 제한 시도, 공정성 설정을 제공합니다.
기능이 많다는 이유로 synchronized를 모두 교체할 필요는 없습니다.
무한 대기와 순서 정책을 실제로 제어해야 할 때 선택합니다.
기본 ReentrantLock의 공정성 오해
인자 없는 생성자는 비공정 락을 만듭니다.
아래 프로그램의 실제 출력은 wrong-default-fair=true, isFair=false입니다.
이름에 큐가 있다고 FIFO가 자동으로 적용되는 것은 아닙니다.
import java.util.concurrent.locks.ReentrantLock;
public final class DefaultFairLockAssumption {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
System.out.println("wrong-default-fair=" + !lock.isFair() + ", isFair=" + lock.isFair());
}
}공정 모드는 new ReentrantLock(true)로 명시합니다.
공정성은 락 요청 순서를 더 존중하지만 OS 스케줄링과 스레드 시작 순서 전체를 보장하는 업무 FIFO는 아닙니다.
요청 자체가 큐에 들어간 순서가 스케줄러에 따라 달라질 수 있습니다.
Lock API의 획득 정책
lock()은 얻을 때까지 기다리며 대기 중 interrupt를 취소 신호로 처리하지 않습니다.
lockInterruptibly()는 interrupt 시 InterruptedException으로 포기합니다.
tryLock()은 즉시 성공 여부를, 시간 제한 tryLock은 제한 시간 동안의 결과를 반환합니다.
어떤 획득법이든 성공한 경우에만 unlock()해야 합니다.
lock() 직후 try-finally를 열고 finally에서 해제하는 모양이 기본입니다.
시간 제한·immediate try가 false라면 임계 영역과 unlock 모두 실행하지 않습니다.
newCondition()은 해당 락에 연결된 조건 큐를 만듭니다.
Object.wait·notify와 비슷한 대기·신호 기능을 제공하면서 여러 조건을 한 락에 둘 수 있습니다.
조건의 세부 동작은 생산자·소비자 설계에서 별도로 다룹니다.
재진입 락의 중첩 획득
ReentrantLock을 가진 스레드가 같은 락을 다시 획득하면 기다리지 않고 보류 count가 증가합니다.
중첩 메서드가 공통 락을 사용해도 self-교착 상태를 피합니다.
각 획득에 대응하는 unlock이 있어야 다른 스레드가 받을 수 있습니다.
import java.util.concurrent.locks.ReentrantLock;
public final class ReentrantHoldCount {
private final ReentrantLock lock = new ReentrantLock();
void outer() {
lock.lock();
try {
System.out.println("outer-hold=" + lock.getHoldCount());
inner();
} finally {
lock.unlock();
}
}
void inner() {
lock.lock();
try {
System.out.println(
"inner-hold="
+ lock.getHoldCount()
+ ", owned="
+ lock.isHeldByCurrentThread());
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantHoldCount example = new ReentrantHoldCount();
example.outer();
System.out.println("locked-after=" + example.lock.isLocked());
}
}보류 count는 1, 2로 증가하고 모든 finally 뒤에는 locked-after=false가 출력됩니다.
진단 메서드를 동기화 조건으로 사용하면 관찰과 동작 사이에 경합이 생길 수 있으므로 락 소유권 판단은 API 결과와 코드 구조로 정합니다.
모니터와 ReentrantLock 대기
내장 모니터 진입 대기는 Thread.State.BLOCKED입니다.
ReentrantLock은 내부적으로 LockSupport.park와 대기 큐를 사용하므로 WAITING, 시간 제한 획득에서는 TIMED_WAITING으로 보일 수 있습니다.
스레드 덤프에서 상태 이름만 보고 같은 락이라고 결론 내리지 않습니다.
unlock은 락을 반납하고 큐의 대기자 하나를 깨울 수 있습니다.
깨어난 스레드는 RUNNABLE이 된 뒤 실제 획득을 다시 시도합니다.
비공정 모드에서는 새로 도착한 스레드가 먼저 잡을 여지가 있습니다.
공정 모드는 큐 선두를 우선해 기아 위험을 줄입니다.
import java.util.concurrent.locks.ReentrantLock;
public final class FairnessConfigurationReport {
public static void main(String[] args) {
report("throughput", new ReentrantLock());
report("bounded-wait", new ReentrantLock(true));
}
private static void report(String policy, ReentrantLock lock) {
lock.lock();
try {
System.out.println(
policy + ": fair=" + lock.isFair() + ", queue=" + lock.getQueueLength());
} finally {
lock.unlock();
}
}
}큐 length는 estimate이며 정확성 단언에 쓰지 않습니다.
정책 이름은 설계 의도를 남기지만 실제 선택은 경합 벤치마크와 최대 대기 시간에서 확인합니다.
공정 모드의 처리량·꼬리 지연 절충
비공정 락은 릴리스 직후 running 스레드가 다시 획득하거나 새 스레드가 barging해 문맥 switch를 줄일 수 있어 처리량에 유리할 수 있습니다.
대신 오래 기다리는 스레드가 생길 가능성이 있습니다.
공정 락은 큐 순서를 더 지켜 대기 편차를 줄이지만 스케줄러 handoff와 캐시 지역성 비용이 늘 수 있습니다.
공정성이 필요한지 평균 처리량만 보지 말고 최대값 wait, p99 지연 시간, 기아 허용 여부를 봅니다.
사용자별 업무 순서를 보장해야 한다면 락 공정성만으로 부족하고 작업 큐에서 키별 순서를 구현해야 합니다.
실험에서는 동일한 임계 영역과 경합 수준에서 공정 설정만 바꾸고 획득 지연 시간 분포를 수집합니다. 짧은 실행의 스레드 이름 순서는 스케줄러 잡음이 커서 공정성 증거가 아닙니다. 오래 기다린 요청의 최대 시간과 단위 시간당 완료 수를 함께 보아야 기아 감소와 처리량 비용을 동시에 판단할 수 있습니다.
주의할 점은 인자 없는 tryLock()이 공정 락에서도 큐를 우회해 즉시 획득할 수 있다는 API 사용 규칙입니다.
공정성을 엄격히 존중하는 시간 제한 시도는 tryLock(0, unit) 같은 중단 가능 시간 제한 형태를 검토하되 문서를 기준으로 사용합니다.
Lock을 도입할지 결정하는 질문
단순한 짧은 임계 영역이고 시간 제한·중단 가능 획득이 필요 없다면 synchronized가 누락 위험이 적습니다.
획득 실패를 업무 결과로 처리하거나 락 wait를 취소해야 하면 ReentrantLock이 적합합니다.
공정성은 실제 기아 요구가 있을 때만 켭니다.
Lock 필드는 private final로 두어 보호 대상과 식별 정보가 바뀌지 않게 합니다.
외부에 노출한 락을 호출자가 임의로 잡으면 class 내부 메서드가 예측 못 한 대기를 겪습니다.
여러 필드의 불변식은 같은 락 아래 일관되게 접근합니다.
연습 문제
첫 인수가 fair이면 공정 락, 그 외에는 기본 비공정 락을 만들고 설정과 보류 count를 출력하세요.
락은 반드시 finally에서 해제합니다.
정답과 설정 경계
생성자에서 한 번 결정한 공정성은 이후 바뀌지 않습니다. 실행 시점 전환이 필요하면 락을 교체하지 말고 상위 구성 요소를 재구성해야 합니다.
import java.util.concurrent.locks.ReentrantLock;
public final class ConfiguredReentrantLockSolution {
public static void main(String[] args) {
boolean fair = args.length > 0 && args[0].equalsIgnoreCase("fair");
ReentrantLock lock = new ReentrantLock(fair);
lock.lock();
try {
System.out.println("fair=" + lock.isFair() + ", hold=" + lock.getHoldCount());
} finally {
lock.unlock();
}
System.out.println("released=" + !lock.isLocked());
}
}기본 실행은 공정=false, 공정 인수 실행은 true입니다.
두 경우 모두 released=true여야 합니다.
ReentrantLock은 synchronized보다 우월한 대체품이 아니라 획득 정책을 더 많이 노출하는 도구입니다.
재진입 횟수, 큐 상태, 공정성 비용, 성공한 획득만 해제하는 규칙을 이해한 뒤 필요한 제어가 있을 때 선택합니다.