본문 바로가기

ReentrantLock과 공정성

기본 비공정 설정과 재진입 횟수를 실행하고 synchronized보다 넓은 Lock 제어 기능의 비용을 판단합니다.

Lock 인터페이스는 획득과 해제를 메서드로 분리하고, 대표 구현인 ReentrantLock은 중단 가능 대기, 즉시·시간 제한 시도, 공정성 설정을 제공합니다.

기능이 많다는 이유로 synchronized를 모두 교체할 필요는 없습니다.

무한 대기와 순서 정책을 실제로 제어해야 할 때 선택합니다.


기본 ReentrantLock의 공정성 오해

인자 없는 생성자는 비공정 락을 만듭니다.

아래 프로그램의 실제 출력은 wrong-default-fair=true, isFair=false입니다.

이름에 큐가 있다고 FIFO가 자동으로 적용되는 것은 아닙니다.

lab/DefaultFairLockAssumption.java
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의 획득 정책

획득 방법에 따라 대기와 중단 결과가 달라진다

ReentrantLock의 네 획득 API를 대기 조건과 반환 결과로 비교합니다. 특정 main의 출력이나 실제 대기 시간 표가 아닙니다.

획득 API · 대기와 반환

획득 방법에 따라 대기와 중단 결과가 달라진다

네 API는 기다림과 중단 처리 방식이 다릅니다. 획득하지 않은 시도에는 unlock 책임이 생기지 않습니다.

네 획득 정책의 요청과 반환 네영역은독립API계약입니다. 성공획득분만해제하고시간제한false·중단예외·즉시false를구별하며이전에보유한재진입책임은별도입니다. lock() 요청과 대기 다른소유자면 획득까지 interrupt로 포기 안 함 결과와 해제 획득 후 임계영역 그 획득분을 unlock lockInterruptibly 요청과 대기 다른소유자면 획득·중단 중단은 예외 / 플래그 소거 결과와 해제 획득한 경로만 unlock 기존획득분 책임은 별도 tryLock() 요청과 대기 기다리지 않고 한 번 시도 true 또는 false 결과와 해제 true인 경로만 unlock false는 업무 대체 경로 시간 제한 tryLock 요청과 대기 획득·기한·중단 중 결과 true / false / 예외 결과와 해제 true인 경로만 unlock false·예외는 미획득 실선: 각API의 요청→결과 관계 영역끼리 순차로 호출하는 그림 아님 실제업무성공·시간보장은 별도
네 획득 정책의 요청과 반환 네영역은독립API계약입니다. 성공획득분만해제하고시간제한false·중단예외·즉시false를구별하며이전에보유한재진입책임은별도입니다. lock() 요청과 대기 다른소유자면 획득까지 interrupt로 포기 안 함 결과와 해제 획득 후 임계영역 그 획득분을 unlock lockInterruptibly 요청과 대기 다른소유자면 획득·중단 중단은 예외 / 플래그 소거 결과와 해제 획득한 경로만 unlock 기존획득분 책임은 별도 tryLock() 요청과 대기 기다리지 않고 한 번 시도 true 또는 false 결과와 해제 true인 경로만 unlock false는 업무 대체 경로 시간 제한 tryLock 요청과 대기 획득·기한·중단 중 결과 true / false / 예외 결과와 해제 true인 경로만 unlock false·예외는 미획득 실선: 각API의 요청→결과 관계 영역끼리 순차로 호출하는 그림 아님 실제업무성공·시간보장은 별도
획득 방법에 따라 대기와 중단 결과가 달라진다
획득 API락을 바로 얻을 수 없을 때반환과 중단
lock()획득할 때까지 대기획득 후 반환 · interrupt만으로 포기하지 않음
lockInterruptibly()획득 또는 interrupt까지 대기획득 후 반환 / InterruptedException
tryLock()기다리지 않음즉시 true 또는 false
tryLock(time, unit)획득·시간 만료·interrupt까지 대기true / false / InterruptedException
lock()
락을 바로 얻을 수 없을 때
획득할 때까지 대기
반환과 중단
획득 후 반환 · interrupt만으로 포기하지 않음
lockInterruptibly()
락을 바로 얻을 수 없을 때
획득 또는 interrupt까지 대기
반환과 중단
획득 후 반환 / InterruptedException
tryLock()
락을 바로 얻을 수 없을 때
기다리지 않음
반환과 중단
즉시 true 또는 false
tryLock(time, unit)
락을 바로 얻을 수 없을 때
획득·시간 만료·interrupt까지 대기
반환과 중단
true / false / InterruptedException

처음 얻으려던 락을 획득하지 못한 예외 경로나 false 반환에는 unlock을 대응시키지 않습니다. 재진입 전에 이미 보유한 획득분의 해제 책임은 별도로 남습니다.

lock() 직후 try-finally를 열고 finally에서 해제하는 모양이 기본입니다.

해제할 획득분이 생겼는지 먼저 판단합니다. tryLock()은 true인 분기 안에서 try-finally를 열고, false이면 보호 상태에 접근하지 않고 실패 결과를 처리합니다. lockInterruptibly()나 시간 제한 시도가 InterruptedException으로 끝나면 이번 호출의 새 획득분은 없습니다. 다만 호출 전에 이미 소유하던 획득분은 그 획득을 담당한 바깥 코드가 계속 해제해야 합니다. 소유하지 않은 스레드의 unlock()은 IllegalMonitorStateException을 일으킵니다.

newCondition()은 해당 락에 연결된 조건 큐를 만듭니다.

Object.wait·notify와 비슷한 대기·신호 기능을 제공하면서 여러 조건을 한 락에 둘 수 있습니다.

조건의 세부 동작은 생산자·소비자 설계에서 별도로 다룹니다.


재진입 락의 중첩 획득

같은 스레드의 중첩 메서드가 공통 락을 사용하는 예제를 살펴봅니다.

src/ReentrantHoldCount.java
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());
    }
}

중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다

한 스레드의 재진입 락 획득 횟수 0→1→2→1→0과 finally 반납의 실제 순서입니다. 같은 락·소유자에서 획득분 균형을 따라가기 좋습니다.

중첩 획득 두 번을 각각 해제해야 보류 횟수가 0이 된다

ReentrantHoldCount의 0, 1, 2, 1, 0 보류 횟수와 실제 출력이 있는 지점을 구분합니다.

재진입 · 획득과 해제

중첩 획득 두 번을 각각 해제해야 보류 횟수가 0이 된다

같은 스레드가 두 번 얻은 락은 두 번 해제합니다. 보류 횟수 0→1→2→1→0을 코드 지점과 함께 읽습니다.

같은 스레드의 획득분을 반납원outer/inner/finally에따라보유횟수0→1→2→1→0입니다. 출력없는inner반납시점과출력지점세개를구별하고대기자수로읽지않습니다.
처음에는0현재 흐름의 획득 없음
연결 관계처음에는0 · 현재 흐름의 획득 없음 → outer가 획득 · outer-hold=1
outer가 획득outer-hold=1
연결 관계outer가 획득 · outer-hold=1 → inner가 재진입 · inner-hold=2 · owned=true
inner가 재진입inner-hold=2owned=true
연결 관계inner가 재진입 · inner-hold=2 · owned=true → inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음
inner의 finally한 번 반납 / 아직1이 지점은 출력 없음
연결 관계inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음 → outer의 finally · 남은1도 반납 / 이제0 · locked-after=false
outer의 finally남은1도 반납 / 이제0locked-after=false

실선: 같은 스레드의 중첩·복귀hold는 현재흐름의 보유 횟수대기자수·획득 순서의 증거 아님

같은 스레드의 획득분을 반납원outer/inner/finally에따라보유횟수0→1→2→1→0입니다. 출력없는inner반납시점과출력지점세개를구별하고대기자수로읽지않습니다.
처음에는0현재 흐름의 획득 없음
연결 관계처음에는0 · 현재 흐름의 획득 없음 → outer가 획득 · outer-hold=1
outer가 획득outer-hold=1
연결 관계outer가 획득 · outer-hold=1 → inner가 재진입 · inner-hold=2 · owned=true
inner가 재진입inner-hold=2owned=true
연결 관계inner가 재진입 · inner-hold=2 · owned=true → inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음
inner의 finally한 번 반납 / 아직1이 지점은 출력 없음
연결 관계inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음 → outer의 finally · 남은1도 반납 / 이제0 · locked-after=false
outer의 finally남은1도 반납 / 이제0locked-after=false

실선: 같은 스레드의 중첩·복귀hold는 현재흐름의 보유 횟수대기자수·획득 순서의 증거아님

  1. 획득 전

    현재 스레드의 보류 횟수
    0
    출력 또는 코드 근거
    새 락 · 아직 획득 호출 없음
  2. outer()의 획득 뒤

    현재 스레드의 보류 횟수
    1
    출력 또는 코드 근거
    outer-hold=1 출력
  3. inner()의 재진입 뒤

    현재 스레드의 보류 횟수
    2
    출력 또는 코드 근거
    inner-hold=2, owned=true 출력
  4. inner()의 해제 뒤

    현재 스레드의 보류 횟수
    1
    출력 또는 코드 근거
    안쪽 finally의 해제 1회 · 이 지점은 출력 없음
  5. outer()의 해제 뒤

    현재 스레드의 보류 횟수
    0
    출력 또는 코드 근거
    남은 획득분 해제 · 이후 locked-after=false 출력

이 예제는 한 스레드가 같은 락을 중첩 획득합니다. 다른 스레드의 대기 순서나 여러 락 사이의 교착 상태를 시험하지 않습니다.

이 예의 조건

  • hold는 현재 스레드 보유 횟수이며 대기자 수가 아닙니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원래 outer/inner/finally에 따라 락 획득 횟수는 0→1→2→1→0입니다. 출력 없는 inner 반납 시점과 출력 지점 세 개를 구별하고 대기자 수로 읽지 않습니다.
3처음에는 0
11한 번 반납
12아직 1
15남은 1도 반납
16이제 0
19hold는 현재 흐름의 보유 횟수
20대기자 수·획득 순서의 증거 아님
원 도표의 설명과 주석
구분내용
설명원래 outer/inner/finally에 따라 락 획득 횟수는 0→1→2→1→0입니다. 출력 없는 inner 반납 시점과 출력 지점 세 개를 구별하고 대기자 수로 읽지 않습니다.
전체 읽기 정보중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 ReentrantHoldCount의 0, 1, 2, 1, 0 락 획득 횟수와 실제 출력이 있는 지점을 구분합니다. 재진입 · 획득과 해제 중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 같은 스레드가 두 번 얻은 락은 두 번 해제합니다. 락 획득 횟수 0→1→2→1→0을 코드 지점과 함께 읽습니다. 획득 전 현재 스레드의 락 획득 횟수 0 출력 또는 코드 근거 새 락 · 아직 획득 호출 없음 outer() 의 획득 뒤 현재 스레드의 락 획득 횟수 1 출력 또는 코드 근거 outer-hold=1 출력 inner() 의 재진입 뒤 현재 스레드의 락 획득 횟수 2 출력 또는 코드 근거 inner-hold=2, owned=true 출력 inner() 의 해제 뒤 현재 스레드의 락 획득 횟수 1 출력 또는 코드 근거 안쪽 finally의 해제 1회 · 이 지점은 출력 없음 outer() 의 해제 뒤 현재 스레드의 락 획득 횟수 0 출력 또는 코드 근거 남은 획득분 해제 · 이후 locked-after=false 출력 이 예제는 한 스레드가 같은 락을 중첩 획득합니다. 다른 스레드의 대기 순서나 여러 락 사이의 교착 상태를 시험하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 획득

outer가 처음 락을 얻습니다.

  • 이전 hold: 0
  • outer-hold: 1
  • 한 번의 획득이 해제 책임 하나를 만듭니다.
재진입

inner가 같은 스레드로 다시 획득합니다.

  • inner-hold: 2
  • owned: true
  • 소유 스레드 재진입이 보유 횟수를 증가시킵니다.
균형 반납

inner finally가 1로 낮추고 outer finally가 0으로 낮춥니다.

  • inner 반납: 1, 출력 없음
  • outer 반납 뒤: locked-after=false
  • 각 획득분을 각각 반납해야 락이 풀립니다.

중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다

한 스레드의 재진입 락 획득 횟수 0→1→2→1→0과 finally 반납의 실제 순서입니다. 같은 락·소유자에서 획득분 균형을 따라가기 좋습니다.

중첩 획득 두 번을 각각 해제해야 보류 횟수가 0이 된다

ReentrantHoldCount의 0, 1, 2, 1, 0 보류 횟수와 실제 출력이 있는 지점을 구분합니다.

재진입 · 획득과 해제

중첩 획득 두 번을 각각 해제해야 보류 횟수가 0이 된다

같은 스레드가 두 번 얻은 락은 두 번 해제합니다. 보류 횟수 0→1→2→1→0을 코드 지점과 함께 읽습니다.

같은 스레드의 획득분을 반납원outer/inner/finally에따라보유횟수0→1→2→1→0입니다. 출력없는inner반납시점과출력지점세개를구별하고대기자수로읽지않습니다.
처음에는0현재 흐름의 획득 없음
연결 관계처음에는0 · 현재 흐름의 획득 없음 → outer가 획득 · outer-hold=1
outer가 획득outer-hold=1
연결 관계outer가 획득 · outer-hold=1 → inner가 재진입 · inner-hold=2 · owned=true
inner가 재진입inner-hold=2owned=true
연결 관계inner가 재진입 · inner-hold=2 · owned=true → inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음
inner의 finally한 번 반납 / 아직1이 지점은 출력 없음
연결 관계inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음 → outer의 finally · 남은1도 반납 / 이제0 · locked-after=false
outer의 finally남은1도 반납 / 이제0locked-after=false

실선: 같은 스레드의 중첩·복귀hold는 현재흐름의 보유 횟수대기자수·획득 순서의 증거 아님

같은 스레드의 획득분을 반납원outer/inner/finally에따라보유횟수0→1→2→1→0입니다. 출력없는inner반납시점과출력지점세개를구별하고대기자수로읽지않습니다.
처음에는0현재 흐름의 획득 없음
연결 관계처음에는0 · 현재 흐름의 획득 없음 → outer가 획득 · outer-hold=1
outer가 획득outer-hold=1
연결 관계outer가 획득 · outer-hold=1 → inner가 재진입 · inner-hold=2 · owned=true
inner가 재진입inner-hold=2owned=true
연결 관계inner가 재진입 · inner-hold=2 · owned=true → inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음
inner의 finally한 번 반납 / 아직1이 지점은 출력 없음
연결 관계inner의 finally · 한 번 반납 / 아직1 · 이 지점은 출력 없음 → outer의 finally · 남은1도 반납 / 이제0 · locked-after=false
outer의 finally남은1도 반납 / 이제0locked-after=false

실선: 같은 스레드의 중첩·복귀hold는 현재흐름의 보유 횟수대기자수·획득 순서의 증거아님

  1. 획득 전

    현재 스레드의 보류 횟수
    0
    출력 또는 코드 근거
    새 락 · 아직 획득 호출 없음
  2. outer()의 획득 뒤

    현재 스레드의 보류 횟수
    1
    출력 또는 코드 근거
    outer-hold=1 출력
  3. inner()의 재진입 뒤

    현재 스레드의 보류 횟수
    2
    출력 또는 코드 근거
    inner-hold=2, owned=true 출력
  4. inner()의 해제 뒤

    현재 스레드의 보류 횟수
    1
    출력 또는 코드 근거
    안쪽 finally의 해제 1회 · 이 지점은 출력 없음
  5. outer()의 해제 뒤

    현재 스레드의 보류 횟수
    0
    출력 또는 코드 근거
    남은 획득분 해제 · 이후 locked-after=false 출력

이 예제는 한 스레드가 같은 락을 중첩 획득합니다. 다른 스레드의 대기 순서나 여러 락 사이의 교착 상태를 시험하지 않습니다.

이 예의 조건

  • hold는 현재 스레드 보유 횟수이며 대기자 수가 아닙니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원래 outer/inner/finally에 따라 락 획득 횟수는 0→1→2→1→0입니다. 출력 없는 inner 반납 시점과 출력 지점 세 개를 구별하고 대기자 수로 읽지 않습니다.
3처음에는 0
11한 번 반납
12아직 1
15남은 1도 반납
16이제 0
19hold는 현재 흐름의 보유 횟수
20대기자 수·획득 순서의 증거 아님
원 도표의 설명과 주석
구분내용
설명원래 outer/inner/finally에 따라 락 획득 횟수는 0→1→2→1→0입니다. 출력 없는 inner 반납 시점과 출력 지점 세 개를 구별하고 대기자 수로 읽지 않습니다.
전체 읽기 정보중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 ReentrantHoldCount의 0, 1, 2, 1, 0 락 획득 횟수와 실제 출력이 있는 지점을 구분합니다. 재진입 · 획득과 해제 중첩 획득 두 번을 각각 해제해야 락 획득 횟수가 0이 된다 같은 스레드가 두 번 얻은 락은 두 번 해제합니다. 락 획득 횟수 0→1→2→1→0을 코드 지점과 함께 읽습니다. 획득 전 현재 스레드의 락 획득 횟수 0 출력 또는 코드 근거 새 락 · 아직 획득 호출 없음 outer() 의 획득 뒤 현재 스레드의 락 획득 횟수 1 출력 또는 코드 근거 outer-hold=1 출력 inner() 의 재진입 뒤 현재 스레드의 락 획득 횟수 2 출력 또는 코드 근거 inner-hold=2, owned=true 출력 inner() 의 해제 뒤 현재 스레드의 락 획득 횟수 1 출력 또는 코드 근거 안쪽 finally의 해제 1회 · 이 지점은 출력 없음 outer() 의 해제 뒤 현재 스레드의 락 획득 횟수 0 출력 또는 코드 근거 남은 획득분 해제 · 이후 locked-after=false 출력 이 예제는 한 스레드가 같은 락을 중첩 획득합니다. 다른 스레드의 대기 순서나 여러 락 사이의 교착 상태를 시험하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 획득

outer가 처음 락을 얻습니다.

  • 이전 hold: 0
  • outer-hold: 1
  • 한 번의 획득이 해제 책임 하나를 만듭니다.
재진입

inner가 같은 스레드로 다시 획득합니다.

  • inner-hold: 2
  • owned: true
  • 소유 스레드 재진입이 보유 횟수를 증가시킵니다.
균형 반납

inner finally가 1로 낮추고 outer finally가 0으로 낮춥니다.

  • inner 반납: 1, 출력 없음
  • outer 반납 뒤: locked-after=false
  • 각 획득분을 각각 반납해야 락이 풀립니다.

진단 메서드를 동기화 조건으로 사용하면 관찰과 동작 사이에 경합이 생길 수 있으므로 락 소유권 판단은 API 결과와 코드 구조로 정합니다.

hold는 다른 스레드가 기다리는 수가 아니라 현재 스레드가 얻고 아직 반납하지 않은 횟수입니다. 원 출력 outer-hold=1 → inner-hold=2 뒤 안쪽 finally가 1회 반납해 1이 남고, 바깥 finally까지 실행하면 0입니다. 안쪽 메서드 반환만으로 다른 스레드가 진입할 수 있는 것은 아닙니다. getQueueLength의 추정 대기자 수와 이 횟수를 구별하세요.


모니터와 ReentrantLock 대기

내장 모니터 진입 대기는 Thread.State.BLOCKED입니다.

ReentrantLock은 내부적으로 LockSupport.park와 대기 큐를 사용하므로 WAITING, 시간 제한 획득에서는 TIMED_WAITING으로 보일 수 있습니다.

스레드 덤프에서 상태 이름만 보고 같은 락이라고 결론 내리지 않습니다.

unlock은 락을 반납하고 큐의 대기자 하나를 깨울 수 있습니다.

깨어난 스레드는 RUNNABLE이 된 뒤 실제 획득을 다시 시도합니다.

비공정 모드에서는 새로 도착한 스레드가 먼저 잡을 여지가 있습니다.

공정 모드는 큐 선두를 우선해 기아 위험을 줄입니다.

src/FairnessConfigurationReport.java
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();
        }
    }
}
두 정책 이름은 설정값을 보여 주며 성능을 측정하지 않는다

FairnessConfigurationReport의 두 생성자와 실제 설정 출력을 비교하되 정책 이름을 벤치마크 결과로 읽지 않습니다.

공정성 · 설정과 측정

두 정책 이름은 설정값을 보여 주며 성능을 측정하지 않는다

두 이름은 생성자 설정을 보여 줍니다. 요청자가 없는 이 출력으로 처리량이나 대기 상한을 판단하지 않습니다.

공정 생성자와 실제 설정 조회 throughput과bounded-wait는설정이름입니다. 두락에다른요청자가없어false/true및queue0을출력할뿐대기상한·FIFO·처리량측정이아닙니다. throughput 설정 이름 기본 생성자 new ReentrantLock() 원 설정 보고 fair=false / queue=0 bounded-wait 설정 이름 공정 생성자 new ReentrantLock(true) 원 설정 보고 fair=true / queue=0 실선: 선택한 생성자→설정 조회 대기자가 없는 두 독립 락 처리량·대기 상한·FIFO 측정 아님
공정 생성자와 실제 설정 조회 throughput과bounded-wait는설정이름입니다. 두락에다른요청자가없어false/true및queue0을출력할뿐대기상한·FIFO·처리량측정이아닙니다. throughput 설정 이름 기본 생성자 new ReentrantLock() 원 설정 보고 fair=false / queue=0 bounded-wait 설정 이름 공정 생성자 new ReentrantLock(true) 원 설정 보고 fair=true / queue=0 실선: 선택한 생성자→설정 조회 대기자가 없는 두 독립 락 처리량·대기 상한·FIFO 측정 아님

throughput

생성자
new ReentrantLock()
출력된 설정과 큐
fair=false · queue=0

bounded-wait

생성자
new ReentrantLock(true)
출력된 설정과 큐
fair=true · queue=0

두 락에는 다른 요청자가 없습니다. queue는 일반적으로 추정치이며, 이 출력은 처리량·대기 시간·FIFO 순서를 측정하지 않습니다.


공정 모드의 처리량·꼬리 지연 절충

비공정 락은 해제 직후 실행 중인 스레드가 다시 획득하거나 새 스레드가 대기 큐를 앞질러 획득해 문맥 전환을 줄일 수 있어 처리량에 유리할 수 있습니다.

대신 오래 기다리는 스레드가 생길 가능성이 있습니다.

공정 락은 큐 순서를 더 지켜 대기 편차를 줄이지만 스케줄러의 실행권 인계와 캐시 지역성 비용이 늘 수 있습니다.

공정성이 필요한지 평균 처리량만 보지 말고 최대 대기 시간, p99 지연 시간, 기아 허용 여부를 봅니다.

사용자별 업무 순서를 보장해야 한다면 락 공정성만으로 부족하고 작업 큐에서 키별 순서를 구현해야 합니다.

실험에서는 동일한 임계 영역과 경합 수준에서 공정 설정만 바꾸고 획득 지연 시간 분포를 수집합니다.

짧은 실행의 스레드 이름 순서는 스케줄러 잡음이 커서 공정성 증거가 아닙니다.

오래 기다린 요청의 최대 시간과 단위 시간당 완료 수를 함께 보아야 기아 감소와 처리량 비용을 동시에 판단할 수 있습니다.

주의할 점은 인자 없는 tryLock()이 공정 락에서도 큐를 우회해 즉시 획득할 수 있다는 API 사용 규칙입니다.

공정성을 엄격히 존중하는 시간 제한 시도는 tryLock(0, unit) 같은 중단 가능 시간 제한 형태를 검토하되 문서를 기준으로 사용합니다.

시간 제한은 락을 얻기 위해 기다리는 예산입니다. tryLock(100, MILLISECONDS)가 80ms 후 성공하고 그 뒤 작업이 300ms 걸린다는 가정이면 완료까지의 합은 약 380ms입니다. 이 호출은 작업을 100ms에서 중단시키지 않으며, 스케줄링 지연까지 포함한 정확한 반환 시각도 보장하지 않습니다. 전체 요청 기한은 획득 이후 작업에도 별도로 적용해야 합니다.


Lock을 도입할지 결정하는 질문

단순한 짧은 임계 영역이고 시간 제한·중단 가능 획득이 필요 없다면 synchronized가 누락 위험이 적습니다.

획득 실패를 업무 결과로 처리하거나 락 획득 대기를 취소해야 하면 ReentrantLock이 적합합니다.

공정성은 실제 기아 요구가 있을 때만 켭니다.

Lock 필드는 private final로 두어 보호 대상과 식별 정보가 바뀌지 않게 합니다.

외부에 노출한 락을 호출자가 임의로 잡으면 class 내부 메서드가 예측 못 한 대기를 겪습니다.

여러 필드의 불변식은 같은 락 아래 일관되게 접근합니다.

같은 락은 같은 객체를 뜻합니다. 두 메서드가 각각 new ReentrantLock(true)를 만들면 공정성 설정이 같아도 서로의 진입을 막지 않습니다. 또한 synchronized(lock)은 그 객체의 내장 모니터를 얻는 것이며 lock.lock()과 연결된 획득이 아닙니다. 하나의 보호 대상에는 공유한 락 인스턴스와 그 락을 사용하는 일관된 접근 규칙이 필요합니다.


연습 문제

첫 인수가 fair이면 공정 락, 그 외에는 기본 비공정 락을 만들고 설정과 락 획득 횟수를 출력하세요.

락은 반드시 finally에서 해제합니다.

정답과 설정 경계

생성자에서 한 번 결정한 공정성은 이후 바뀌지 않습니다.

실행 시점 전환이 필요하면 락을 교체하지 말고 상위 구성 요소를 재구성해야 합니다.

exercise/ConfiguredReentrantLockSolution.java
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여야 합니다.

직접 검산할 입력은 인자 없음과 fair 두 실행입니다. 두 실행의 hold는 모두 1이고 마지막 released는 true이며, 설정 fair만 false·true로 달라집니다. 이 단일 스레드 결과에는 대기자가 없으므로 이름 bounded-wait나 queue=0을 대기 시간 상한의 증거로 쓰지 않습니다.

ReentrantLock은 synchronized보다 우월한 대체품이 아니라 획득 정책을 더 많이 노출하는 도구입니다.

재진입 횟수, 큐 상태, 공정성 비용, 성공한 획득만 해제하는 규칙을 이해한 뒤 필요한 제어가 있을 때 선택합니다.

획득 결과를 따라가는 연습

하나의 공정 ReentrantLock에 대해 아래 세 상황을 각각 독립적으로 판단하세요. 다른 동시 호출은 없다고 가정합니다.

  1. 현재 스레드의 락 획득 횟수가 1입니다. 이 스레드가 tryLock()을 호출하고 성공분을 한 번 해제합니다. 반환값과 해제 후 횟수는 무엇이며 다른 스레드가 이제 획득할 수 있나요?
  2. 스레드 A가 락을 소유한 동안 소유하지 않은 B가 인자 없는 tryLock()을 호출합니다. B의 반환값·락 획득 횟수와, B가 실패 분기에서 unlock()을 잘못 호출할 때의 결과를 답하세요.
  3. 락은 비어 있고 현재 스레드는 소유자가 아닙니다. 호출 직전에 중단 상태가 true입니다. 인자 없는 tryLock()과 tryLock(0, MILLISECONDS)를 서로 독립적으로 실행할 때 획득 결과·중단 상태·해제 책임을 비교하세요.
정답과 획득분 추적
  1. 재진입이므로 true, 횟수는 1→2→1입니다. 안쪽 성공분을 해제해도 바깥 획득분 1이 남아 다른 스레드는 아직 획득할 수 없습니다. 바깥 코드도 한 번 해제해야 0이 됩니다.
  2. B는 즉시 false를 받고 B의 락 획득 횟수는 0입니다. B의 unlock()은 IllegalMonitorStateException을 일으키며 A의 획득분을 대신 반납하지 않습니다. 실패 분기에서는 보호 상태에 접근하거나 새 해제를 수행하지 않습니다.
  3. 인자 없는 형태는 이 상황에서 true로 획득하고 중단 상태를 그대로 유지하므로 성공분을 한 번 해제해야 합니다. 0시간 형태는 중단을 먼저 처리해 InterruptedException을 던지고 중단 상태를 지우며 새 획득분이 없습니다. 두 형태가 모두 기다리지 않는다고 해서 중단 처리 계약까지 같지는 않습니다.