본문 바로가기

tryLock과 획득 실패

획득 실패 뒤 무조건 unlock하는 예외를 재현하고 즉시·시간 제한 tryLock의 바쁜 대기, 제한 시간, interrupt 결과를 설계합니다.

tryLock은 락을 무한히 기다리지 않고 락 획득 실패를 정상적인 업무 결과로 다룰 수 있게 합니다.

즉시 형태는 지금 비어 있는지 한 번 보고, 시간 제한 형태는 정한 예산 안에서 기다립니다.

반환값을 무시하면 임계 영역을 락 없이 실행하거나 소유하지 않은 락을 해제하게 됩니다.


잠금 획득 결과와 해제 조건

다음 예제는 획득 결과와 관계없이 unlock하는 잘못된 구조입니다.

lab/UnlockWithoutAcquisitionFailure.java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

public final class UnlockWithoutAcquisitionFailure {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        CountDownLatch acquired = new CountDownLatch(1);
        Thread holder =
                new Thread(
                        () -> {
                            lock.lock();
                            try {
                                acquired.countDown();
                                Thread.sleep(50);
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            } finally {
                                lock.unlock();
                            }
                        },
                        "short-holder");
        holder.start();
        acquired.await();
        boolean mine = lock.tryLock();
        try {
            System.out.println("acquired=" + mine);
        } finally {
            lock.unlock();
        }
    }
}

획득 실패 뒤 unlock하면 소유권 예외가 난다

tryLock의 false/true에 따라 무조건 unlock의 예외/정상 결과가 갈립니다. sleep이 순서를 보장하지 않는 조건 아래 획득→해제 책임을 추적합니다.

획득 실패 뒤 unlock하면 소유권 예외가 난다

UnlockWithoutAcquisitionFailure에서 실제 관측한 false와 IllegalMonitorStateException, 원문상 가능한 true 정상 종료를 구분합니다.

해제 조건 · 소유권

획득 실패 뒤 unlock하면 소유권 예외가 난다

false를 받은 시도는 락을 소유하지 않습니다. 무조건 unlock하면 예외가 나며, 지연되어 true를 받은 경로도 가능합니다.

획득 결과와 소유권 예외의 분기 sleep50은시도까지보유를고정하지않아false예외와지연된true정상해제가둘다가능합니다. 실제소유한획득분만반납해야합니다. main의 tryLock sleep50은 보유 보장 아님 mine 결과? false · 미소유 무조건 unlock에서 IllegalMonitorState Exception 발생 true · 소유 holder가 먼저 반납한 경우 자기 획득을 정상 해제 false true 실선: 시도 결과에 따른 분기 · 거절한 획득에는unlock책임이 없음 같은 원코드라도 main이 늦으면true가 가능 · 한 번의 예외를고정출력으로 해석하지 않음
획득 결과와 소유권 예외의 분기 sleep50은시도까지보유를고정하지않아false예외와지연된true정상해제가둘다가능합니다. 실제소유한획득분만반납해야합니다. main의 tryLock sleep50은 보유 보장 아님 mine 결과? false · 미소유 무조건 unlock에서 IllegalMonitorState Exception 발생 true · 소유 holder가 먼저 반납한 경우 자기 획득을 정상 해제 false true 실선: 실제시도 결과의 두 분기 false일 때만 소유권 예외 지연된true경로도 원소스에 존재

acquired=false

그 시도의 소유권
시도 때 소유자가 아직 보유 · main은 미소유
무조건 unlock하는 원문의 결과
IllegalMonitorStateException · 이번 실행에서 관측

acquired=true

그 시도의 소유권
소유자가 먼저 해제 · main이 획득
무조건 unlock하는 원문의 결과
정상 해제 가능 · 이번 실행에서는 미관측

래치는 소유자가 한 번 획득했음을 알릴 뿐, main의 시도까지 보유를 유지하지 않습니다. main이 지연되어 소유자가 먼저 해제하면 두 번째 경로도 가능합니다.

이 예의 조건

  • sleep 50은 main 시도까지 holder 보유를 보장하지 않습니다. false·true는 대안입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2sleep 50은 시도까지 보유를 고정하지 않아 false의 예외와 지연된 true의 정상 해제가 둘 다 가능합니다. 실제 소유한 획득분만 반납해야 합니다.
4sleep 50은 보유 보장 아님
15실선: 시도 결과에 따른 분기 · 거절한 획득에는 unlock 책임이 없음
16같은 원래 코드라도 main이 늦으면 true가 가능 · 한 번의 예외를 고정 출력으로 해석하지 않음
원 도표의 설명과 주석
구분내용
설명sleep 50은 시도까지 보유를 고정하지 않아 false의 예외와 지연된 true의 정상 해제가 둘 다 가능합니다. 실제 소유한 획득분만 반납해야 합니다.
전체 읽기 정보획득 실패 뒤 unlock하면 소유권 예외가 난다 획득 실패 뒤 unlock하면 소유권 예외가 난다 UnlockWithoutAcquisitionFailure에서 실제 관측한 false와 IllegalMonitorStateException, 원문상 가능한 true 정상 종료를 구분합니다. 해제 조건 · 소유권 획득 실패 뒤 unlock하면 소유권 예외가 난다 false를 받은 시도는 락을 소유하지 않습니다. 무조건 unlock하면 예외가 나며, 지연되어 true를 받은 경로도 가능합니다. acquired=false 그 시도의 소유권 시도 때 소유자가 아직 보유 · main은 미소유 무조건 unlock하는 원문의 결과 IllegalMonitorStateException · 이번 실행에서 관측 acquired=true 그 시도의 소유권 소유자가 먼저 해제 · main이 획득 무조건 unlock하는 원문의 결과 정상 해제 가능 · 이번 실행에서는 미관측 래치는 소유자가 한 번 획득했음을 알릴 뿐, main의 시도까지 보유를 유지하지 않습니다. main이 지연되어 소유자가 먼저 해제하면 두 번째 경로도 가능합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 즉시 시도

main의 tryLock 시점과 holder 반납 시점이 갈립니다.

  • 반납 전 시도: mine=false, 미소유
  • 반납 뒤 시도: mine=true, 소유
  • 스케줄 지연으로 같은 main 코드의 획득 결과가 달라집니다.
무조건 반납 호출

원 코드가 두 결과 모두에서 unlock을 호출합니다.

  • mine=false: 남의 락을 해제하려는 호출
  • mine=true: 자기 획득분 해제 호출
  • 반납할 권한은 시도 여부가 아닌 획득 성공에 달렸습니다.
각 관찰 결과

미소유·소유 상태의 해제 결과를 비교합니다.

  • false: IllegalMonitorStateException
  • true: 정상 해제
  • 거절된 획득에는 unlock 책임이 없고 예외를 고정 출력으로 단정하지 않습니다.

획득 실패 뒤 unlock하면 소유권 예외가 난다

tryLock의 false/true에 따라 무조건 unlock의 예외/정상 결과가 갈립니다. sleep이 순서를 보장하지 않는 조건 아래 획득→해제 책임을 추적합니다.

획득 실패 뒤 unlock하면 소유권 예외가 난다

UnlockWithoutAcquisitionFailure에서 실제 관측한 false와 IllegalMonitorStateException, 원문상 가능한 true 정상 종료를 구분합니다.

해제 조건 · 소유권

획득 실패 뒤 unlock하면 소유권 예외가 난다

false를 받은 시도는 락을 소유하지 않습니다. 무조건 unlock하면 예외가 나며, 지연되어 true를 받은 경로도 가능합니다.

획득 결과와 소유권 예외의 분기 sleep50은시도까지보유를고정하지않아false예외와지연된true정상해제가둘다가능합니다. 실제소유한획득분만반납해야합니다. main의 tryLock sleep50은 보유 보장 아님 mine 결과? false · 미소유 무조건 unlock에서 IllegalMonitorState Exception 발생 true · 소유 holder가 먼저 반납한 경우 자기 획득을 정상 해제 false true 실선: 시도 결과에 따른 분기 · 거절한 획득에는unlock책임이 없음 같은 원코드라도 main이 늦으면true가 가능 · 한 번의 예외를고정출력으로 해석하지 않음
획득 결과와 소유권 예외의 분기 sleep50은시도까지보유를고정하지않아false예외와지연된true정상해제가둘다가능합니다. 실제소유한획득분만반납해야합니다. main의 tryLock sleep50은 보유 보장 아님 mine 결과? false · 미소유 무조건 unlock에서 IllegalMonitorState Exception 발생 true · 소유 holder가 먼저 반납한 경우 자기 획득을 정상 해제 false true 실선: 실제시도 결과의 두 분기 false일 때만 소유권 예외 지연된true경로도 원소스에 존재

acquired=false

그 시도의 소유권
시도 때 소유자가 아직 보유 · main은 미소유
무조건 unlock하는 원문의 결과
IllegalMonitorStateException · 이번 실행에서 관측

acquired=true

그 시도의 소유권
소유자가 먼저 해제 · main이 획득
무조건 unlock하는 원문의 결과
정상 해제 가능 · 이번 실행에서는 미관측

래치는 소유자가 한 번 획득했음을 알릴 뿐, main의 시도까지 보유를 유지하지 않습니다. main이 지연되어 소유자가 먼저 해제하면 두 번째 경로도 가능합니다.

이 예의 조건

  • sleep 50은 main 시도까지 holder 보유를 보장하지 않습니다. false·true는 대안입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2sleep 50은 시도까지 보유를 고정하지 않아 false의 예외와 지연된 true의 정상 해제가 둘 다 가능합니다. 실제 소유한 획득분만 반납해야 합니다.
4sleep 50은 보유 보장 아님
15실선: 시도 결과에 따른 분기 · 거절한 획득에는 unlock 책임이 없음
16같은 원래 코드라도 main이 늦으면 true가 가능 · 한 번의 예외를 고정 출력으로 해석하지 않음
원 도표의 설명과 주석
구분내용
설명sleep 50은 시도까지 보유를 고정하지 않아 false의 예외와 지연된 true의 정상 해제가 둘 다 가능합니다. 실제 소유한 획득분만 반납해야 합니다.
전체 읽기 정보획득 실패 뒤 unlock하면 소유권 예외가 난다 획득 실패 뒤 unlock하면 소유권 예외가 난다 UnlockWithoutAcquisitionFailure에서 실제 관측한 false와 IllegalMonitorStateException, 원문상 가능한 true 정상 종료를 구분합니다. 해제 조건 · 소유권 획득 실패 뒤 unlock하면 소유권 예외가 난다 false를 받은 시도는 락을 소유하지 않습니다. 무조건 unlock하면 예외가 나며, 지연되어 true를 받은 경로도 가능합니다. acquired=false 그 시도의 소유권 시도 때 소유자가 아직 보유 · main은 미소유 무조건 unlock하는 원문의 결과 IllegalMonitorStateException · 이번 실행에서 관측 acquired=true 그 시도의 소유권 소유자가 먼저 해제 · main이 획득 무조건 unlock하는 원문의 결과 정상 해제 가능 · 이번 실행에서는 미관측 래치는 소유자가 한 번 획득했음을 알릴 뿐, main의 시도까지 보유를 유지하지 않습니다. main이 지연되어 소유자가 먼저 해제하면 두 번째 경로도 가능합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 즉시 시도

main의 tryLock 시점과 holder 반납 시점이 갈립니다.

  • 반납 전 시도: mine=false, 미소유
  • 반납 뒤 시도: mine=true, 소유
  • 스케줄 지연으로 같은 main 코드의 획득 결과가 달라집니다.
무조건 반납 호출

원 코드가 두 결과 모두에서 unlock을 호출합니다.

  • mine=false: 남의 락을 해제하려는 호출
  • mine=true: 자기 획득분 해제 호출
  • 반납할 권한은 시도 여부가 아닌 획득 성공에 달렸습니다.
각 관찰 결과

미소유·소유 상태의 해제 결과를 비교합니다.

  • false: IllegalMonitorStateException
  • true: 정상 해제
  • 거절된 획득에는 unlock 책임이 없고 예외를 고정 출력으로 단정하지 않습니다.

획득에 성공한 분기 안에 해제 책임을 묶어야 합니다.

원 sleep(50)은 main의 시도 때까지 보유를 고정하는 장치가 아닙니다. main이 먼저 실행되면 acquired=false와 소유권 예외가 나오지만, 늦게 실행되어 holder가 이미 반납했으면 true 뒤 정상 해제할 수 있습니다. 아래 ImmediateTryLockService는 release 래치로 보유를 유지하므로 while-held=busy를 결정적으로 검산할 수 있습니다. 실패 이름이나 한 번의 예외 로그 대신 보유 조건이 언제까지 유지되는지를 보세요.


tryLock의 호출자 역압력

대기할 수 없는 UI 요청, 중복 실행 방지, 가능한 경우에만 수행하는 유지보수는 바쁨 결과를 바로 반환할 수 있습니다.

실패가 예외일 필요는 없지만 요청 폐기, 재시도, 큐 중 어떤 정책인지 알려야 합니다.

src/ImmediateTryLockService.java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

public final class ImmediateTryLockService {
    private final ReentrantLock lock = new ReentrantLock();

    String refresh() {
        if (!lock.tryLock()) return "busy";
        try {
            return "refreshed";
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ImmediateTryLockService service = new ImmediateTryLockService();
        CountDownLatch held = new CountDownLatch(1), release = new CountDownLatch(1);
        Thread owner =
                new Thread(
                        () -> {
                            service.lock.lock();
                            try {
                                held.countDown();
                                await(release);
                            } finally {
                                service.lock.unlock();
                            }
                        },
                        "refresh-owner");
        owner.start();
        held.await();
        System.out.println("while-held=" + service.refresh());
        release.countDown();
        owner.join();
        System.out.println("after-release=" + service.refresh());
    }

    private static void await(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

held 래치로 고정한 busy 시도 뒤 release·owner join을 거쳐 다음 tryLock 성공합니다. 실제 획득 조건 변화가 단계형에 적합합니다.

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

ImmediateTryLockService의 두 호출은 sleep 추정이 아니라 release 래치와 owner join으로 구분됩니다.

즉시 시도 · 전후 경계

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

release 래치 전에는 owner가 락을 유지합니다. 래치를 풀고 owner를 회수한 뒤 main이 다시 시도합니다.

보유 래치와 반납 뒤의 즉시 시도held/release래치와ownerjoin은보유중busy와반납뒤refreshed를구분합니다. 소유하지않은분기는해제하지않고재시도정책은이예제밖입니다.
owner가 락 획득held신호 후 release대기
연결 관계owner가 락 획득 · held신호 후 release대기 → main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함
main의 첫 refreshtryLock=falsewhile-held=busyunlock 안 함
연결 관계main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함 → main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수
main이 release해제owner가 unlock후 종료main이 owner.join으로 회수
연결 관계main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수 → main의 다음 refresh · tryLock=true / refreshed · finally에서 자기 획득 해제
main의 다음 refreshtryLock=true / refreshedfinally에서 자기 획득 해제

실선: 래치·종료·시도의 필요 순서sleep50의 추정과 다른 고정 조건즉시거부가 무한재시도를 허용하진 않음

보유 래치와 반납 뒤의 즉시 시도held/release래치와ownerjoin은보유중busy와반납뒤refreshed를구분합니다. 소유하지않은분기는해제하지않고재시도정책은이예제밖입니다.
owner가 락 획득held신호 후 release대기
연결 관계owner가 락 획득 · held신호 후 release대기 → main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함
main의 첫 refreshtryLock=falsewhile-held=busyunlock 안 함
연결 관계main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함 → main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수
main이 release해제owner가 unlock후 종료main이 owner.join으로 회수
연결 관계main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수 → main의 다음 refresh · tryLock=true / refreshed · finally에서 자기 획득 해제
main의 다음 refreshtryLock=true / refreshedfinally에서 자기 획득 해제

실선: 래치·종료·시도의 필요순서sleep50의 추정과 다른 고정조건즉시거부가 무한재시도를허용하진 않음

  1. release 래치 해제 전

    락을 가진 스레드
    owner가 락을 유지함 · main은 획득하지 못함
    반환 출력
    while-held=busy
  2. 래치 해제와 owner join 뒤

    락을 가진 스레드
    main이 획득하고 finally에서 해제함
    반환 출력
    after-release=refreshed

busy는 즉시 거부 결과입니다. 이를 재시도하는 반복문은 이 예제에 없습니다.

이 예의 조건

  • held와 release 래치가 두 시도의 보유 조건을 고정합니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2held/release 래치와 owner join은 보유 중의 busy와 반납 뒤의 refreshed를 구분합니다. 소유하지 않은 분기는 해제하지 않고 재시도 정책은 이 예제 밖입니다.
4held 신호 후 release 대기
9main이 release 해제
10owner가 unlock 후 종료
13tryLock=true
14refreshed
17sleep 50의 추정과 다른 고정 조건
18즉시 거부가 무한 재시도를 허용하진 않음
원 도표의 설명과 주석
구분내용
설명held/release 래치와 owner join은 보유 중의 busy와 반납 뒤의 refreshed를 구분합니다. 소유하지 않은 분기는 해제하지 않고 재시도 정책은 이 예제 밖입니다.
전체 읽기 정보래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 ImmediateTryLockService의 두 호출은 sleep 추정이 아니라 release 래치와 owner join으로 구분됩니다. 즉시 시도 · 전후 경계 래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 release 래치 전에는 owner가 락을 유지합니다. 래치를 풀고 owner를 회수한 뒤 main이 다시 시도합니다. release 래치 해제 전 락을 가진 스레드 owner가 락을 유지함 · main은 획득하지 못함 반환 출력 while-held=busy 래치 해제와 owner join 뒤 락을 가진 스레드 main이 획득하고 finally에서 해제함 반환 출력 after-release=refreshed busy는 즉시 거부 결과입니다. 이를 재시도하는 반복문은 이 예제에 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
보유 중 즉시 시도

owner가 획득 후 held를 알리고 release를 기다립니다.

  • main tryLock: false
  • while-held: busy, unlock 없음
  • 미획득 분기는 남의 락을 해제하지 않습니다.
반납 회수

main이 release를 해제하고 owner.join을 끝냅니다.

  • owner: unlock 후 종료
  • 다음 시도 조건: 반납 완료
  • 래치와 join이 다음 획득 가능 조건을 만듭니다.
다음 즉시 시도

main이 tryLock에 성공하고 refresh를 수행합니다.

  • 결과: refreshed
  • finally: 자기 획득 해제
  • 성공한 획득분만 반납합니다.

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

held 래치로 고정한 busy 시도 뒤 release·owner join을 거쳐 다음 tryLock 성공합니다. 실제 획득 조건 변화가 단계형에 적합합니다.

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

ImmediateTryLockService의 두 호출은 sleep 추정이 아니라 release 래치와 owner join으로 구분됩니다.

즉시 시도 · 전후 경계

래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다

release 래치 전에는 owner가 락을 유지합니다. 래치를 풀고 owner를 회수한 뒤 main이 다시 시도합니다.

보유 래치와 반납 뒤의 즉시 시도held/release래치와ownerjoin은보유중busy와반납뒤refreshed를구분합니다. 소유하지않은분기는해제하지않고재시도정책은이예제밖입니다.
owner가 락 획득held신호 후 release대기
연결 관계owner가 락 획득 · held신호 후 release대기 → main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함
main의 첫 refreshtryLock=falsewhile-held=busyunlock 안 함
연결 관계main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함 → main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수
main이 release해제owner가 unlock후 종료main이 owner.join으로 회수
연결 관계main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수 → main의 다음 refresh · tryLock=true / refreshed · finally에서 자기 획득 해제
main의 다음 refreshtryLock=true / refreshedfinally에서 자기 획득 해제

실선: 래치·종료·시도의 필요 순서sleep50의 추정과 다른 고정 조건즉시거부가 무한재시도를 허용하진 않음

보유 래치와 반납 뒤의 즉시 시도held/release래치와ownerjoin은보유중busy와반납뒤refreshed를구분합니다. 소유하지않은분기는해제하지않고재시도정책은이예제밖입니다.
owner가 락 획득held신호 후 release대기
연결 관계owner가 락 획득 · held신호 후 release대기 → main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함
main의 첫 refreshtryLock=falsewhile-held=busyunlock 안 함
연결 관계main의 첫 refresh · tryLock=false · while-held=busy · unlock 안 함 → main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수
main이 release해제owner가 unlock후 종료main이 owner.join으로 회수
연결 관계main이 release해제 · owner가 unlock후 종료 · main이 owner.join으로 회수 → main의 다음 refresh · tryLock=true / refreshed · finally에서 자기 획득 해제
main의 다음 refreshtryLock=true / refreshedfinally에서 자기 획득 해제

실선: 래치·종료·시도의 필요순서sleep50의 추정과 다른 고정조건즉시거부가 무한재시도를허용하진 않음

  1. release 래치 해제 전

    락을 가진 스레드
    owner가 락을 유지함 · main은 획득하지 못함
    반환 출력
    while-held=busy
  2. 래치 해제와 owner join 뒤

    락을 가진 스레드
    main이 획득하고 finally에서 해제함
    반환 출력
    after-release=refreshed

busy는 즉시 거부 결과입니다. 이를 재시도하는 반복문은 이 예제에 없습니다.

이 예의 조건

  • held와 release 래치가 두 시도의 보유 조건을 고정합니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2held/release 래치와 owner join은 보유 중의 busy와 반납 뒤의 refreshed를 구분합니다. 소유하지 않은 분기는 해제하지 않고 재시도 정책은 이 예제 밖입니다.
4held 신호 후 release 대기
9main이 release 해제
10owner가 unlock 후 종료
13tryLock=true
14refreshed
17sleep 50의 추정과 다른 고정 조건
18즉시 거부가 무한 재시도를 허용하진 않음
원 도표의 설명과 주석
구분내용
설명held/release 래치와 owner join은 보유 중의 busy와 반납 뒤의 refreshed를 구분합니다. 소유하지 않은 분기는 해제하지 않고 재시도 정책은 이 예제 밖입니다.
전체 읽기 정보래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 ImmediateTryLockService의 두 호출은 sleep 추정이 아니라 release 래치와 owner join으로 구분됩니다. 즉시 시도 · 전후 경계 래치 해제 전과 소유자 종료 뒤에 한 번씩 시도한다 release 래치 전에는 owner가 락을 유지합니다. 래치를 풀고 owner를 회수한 뒤 main이 다시 시도합니다. release 래치 해제 전 락을 가진 스레드 owner가 락을 유지함 · main은 획득하지 못함 반환 출력 while-held=busy 래치 해제와 owner join 뒤 락을 가진 스레드 main이 획득하고 finally에서 해제함 반환 출력 after-release=refreshed busy는 즉시 거부 결과입니다. 이를 재시도하는 반복문은 이 예제에 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
보유 중 즉시 시도

owner가 획득 후 held를 알리고 release를 기다립니다.

  • main tryLock: false
  • while-held: busy, unlock 없음
  • 미획득 분기는 남의 락을 해제하지 않습니다.
반납 회수

main이 release를 해제하고 owner.join을 끝냅니다.

  • owner: unlock 후 종료
  • 다음 시도 조건: 반납 완료
  • 래치와 join이 다음 획득 가능 조건을 만듭니다.
다음 즉시 시도

main이 tryLock에 성공하고 refresh를 수행합니다.

  • 결과: refreshed
  • finally: 자기 획득 해제
  • 성공한 획득분만 반납합니다.

호출자가 바쁨 결과를 받고 무제한으로 재시도하면 CPU와 경합을 늘리므로 상한이 있는 큐, 백오프, 사용자 재시도 안내 가운데 하나를 고릅니다.


시간 제한 tryLock 결과

tryLock(time, unit)은 예산 안에 획득하면 true, 시간이 지나면 false, 대기 중 interrupt면 InterruptedException입니다.

false와 예외를 같은 실패 코드로 합치면 재시도 정책과 shutdown 관찰이 흐려집니다.

src/TimedTryLockOutcome.java
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public final class TimedTryLockOutcome {
    private enum Result {
        UPDATED,
        TIMED_OUT,
        INTERRUPTED
    }

    private final ReentrantLock lock = new ReentrantLock();
    private int version;

    Result update(Duration wait) {
        boolean acquired = false;
        try {
            acquired = lock.tryLock(wait.toNanos(), TimeUnit.NANOSECONDS);
            if (!acquired) return Result.TIMED_OUT;
            version++;
            return Result.UPDATED;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.INTERRUPTED;
        } finally {
            if (acquired) lock.unlock();
        }
    }

    public static void main(String[] args) {
        TimedTryLockOutcome service = new TimedTryLockOutcome();
        System.out.println(
                "result=" + service.update(Duration.ofMillis(20)) + ", version=" + service.version);
    }
}

제시한 main은 경합과 중단 요청 없이 실행해 result=UPDATED, version=1을 출력합니다. TIMED_OUT과 INTERRUPTED 분기는 실행하지 않습니다.

획득 여부를 나타내는 boolean 변수는 finally에서 소유 여부를 정확히 반영합니다.

Duration 입력 검증은 이 예제에 없으므로 공개 API로 확장할 때 별도로 정의해야 합니다.


전체 요청 기한과 락 예산

락에 500ms, DB에 500ms, 네트워크에 500ms를 각각 독립으로 주면 총 요청 예산 700ms를 넘습니다.

상위 기한에서 남은 시간을 계산해 획득에 전달하고, 락을 얻은 뒤 하위 작업에도 남은 예산만 줍니다.

시간 제한 tryLock이 false를 반환하면 호출자는 미획득 분기를 실행합니다.

락 소유자의 작업은 계속 진행합니다.

시간 제한 호출자가 같은 업무를 재시도할 때 이전 시도가 커밋되지 않았는지 멱등성을 확인합니다.

공정 락에서 즉시 tryLock은 대기 큐를 건너뛸 수 있습니다.

공정성이 요구라면 시간 제한 형태의 규칙을 확인하고 큐 기반 요청 순서를 별도로 설계합니다.

락 API만으로 전체 처리 과정의 사용자 요청 순서를 보장한다고 주장하지 않습니다.


락 API 선택 기준

짧고 항상 기다려도 되는 임계 영역은 synchronized가 단순합니다.

중단 가능 무한 대기는 lockInterruptibly, 즉시 포기는 tryLock, 제한된 기다림은 시간 제한 tryLock입니다.

공정 모드는 기아 위험과 처리량을 측정한 뒤 별도로 정합니다.

가시성만 필요한 단일 stop 플래그는 volatile이 맞고, 단일 독립 카운터는 원자 타입이 간결할 수 있습니다.

도구 이름이 아니라 보호하는 불변식과 대기 결과에서 출발합니다.


관찰 가능한 실패 결과

BUSY는 동시 처리 중, TIMED_OUT은 예산 소진, INTERRUPTED는 취소 요청, REJECTED는 입력 정책 위반처럼 나눕니다.

지표 레이블이 너무 세밀해 레이블 종류 수가 폭증하지 않게 제한된 enum을 사용합니다.

락 획득 대기 시간의 히스토그램과 락 소유자의 임계 영역 수행 시간을 별도로 측정하면 병목 위치를 찾기 쉽습니다.

예외를 정상 바쁜 대기 흐름에 남발하면 로그 잡음이 커질 수 있습니다.

반대로 false만 반환하면 호출자가 원인을 버릴 수 있으므로 도메인 결과 타입을 둡니다.

다음 예제는 상태 문자열과 카운터를 함께 반환합니다.

app/BoundedPublisher.java
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public final class BoundedPublisher {
    private record Outcome(String status, int published) {}

    private final ReentrantLock lock = new ReentrantLock();
    private int published;

    Outcome publish(Duration wait) {
        boolean acquired = false;
        try {
            acquired = lock.tryLock(wait.toNanos(), TimeUnit.NANOSECONDS);
            if (!acquired) return new Outcome("timed-out", published);
            published++;
            return new Outcome("published", published);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new Outcome("interrupted", published);
        } finally {
            if (acquired) lock.unlock();
        }
    }

    public static void main(String[] args) {
        BoundedPublisher publisher = new BoundedPublisher();
        System.out.println(publisher.publish(Duration.ofMillis(10)));
        System.out.println(publisher.publish(Duration.ZERO));
    }
}

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

성공 경로는 보호된 증가·반환, 실패 분기는 획득 없이 plain 카운터를 읽습니다. 분기별 획득 여부·갱신·읽기·반납을 단계로 설명하되 원 main은 성공만 실행함을 남깁니다.

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

BoundedPublisher의 실제 두 성공 출력과 timed-out/interrupted 소스 분기를 나눕니다. 실패 분기의 plain published 무잠금 조회 결함은 그대로 존재합니다.

결과와 결함 · 미획득 읽기

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

실패 상태를 반환해도 published를 읽는 경계가 안전해지는 것은 아닙니다. 미획득 두 분기의 카운터는 무잠금 조회입니다.

미획득 결과가 읽는 무잠금 카운터 원성공경로1/2와미실행두실패분기를구분합니다. 실패plainpublished읽기는정확한스냅샷이아니며본문별도버전은실패카운터를관측하지않습니다. 원 성공 경로 timed tryLock true 락 안에서 published++ 보호된 값 반환 원main에서1 뒤2 finally가 unlock 원 시간 만료 경로 timed tryLock false 증가·unlock 없음 plain 카운터를 읽음 획득 없이 published반환 정확한 스냅샷 아님 원 중단 경로 획득 중단 예외 플래그 복원 plain 카운터를 읽음 증가·unlock 없음 정확한 스냅샷 아님 실선: 원코드의 분기와 반환 원main은 실패 두 분기 미실행 본문별도버전은 실패숫자를 안 읽음
미획득 결과가 읽는 무잠금 카운터 원성공경로1/2와미실행두실패분기를구분합니다. 실패plainpublished읽기는정확한스냅샷이아니며본문별도버전은실패카운터를관측하지않습니다. 원 성공 경로 timed tryLock true 락 안에서 published++ 보호된 값 반환 원main에서1 뒤2 finally가 unlock 원 시간 만료 경로 timed tryLock false 증가·unlock 없음 plain 카운터를 읽음 획득 없이 published반환 정확한 스냅샷 아님 원 중단 경로 획득 중단 예외 플래그 복원 plain 카운터를 읽음 증가·unlock 없음 정확한 스냅샷 아님 실선: 원코드의 분기와 반환 원main은 실패 두 분기 미실행 본문별도버전은 실패숫자를 안 읽음

published

카운터 변경과 해제
획득 뒤 증가 · finally에서 unlock
원문과 실행의 범위
main의 두 성공 결과: published=1, published=2

timed-out

카운터 변경과 해제
증가와 unlock 없음
원문과 실행의 범위
미실행 분기 · published를 락 없이 읽어 반환

interrupted

카운터 변경과 해제
플래그 복원 · 증가와 unlock 없음
원문과 실행의 범위
미실행 분기 · published를 락 없이 읽어 반환

실패 결과의 published는 다른 스레드의 갱신을 동기화해 읽은 값이 아닙니다. 정확한 실패 스냅샷으로 해석할 수 없습니다. 성공 경로도 숫자 증가만 하며 실제 게시 I/O는 없습니다.

이 예의 조건

  • 원 main은 성공 반환 1·2만 실행합니다. 시간 만료·중단은 미실행 조건입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원래 성공 경로 1/2와 미실행인 두 실패 분기를 구분합니다. 실패의 plain published 읽기는 정확한 스냅샷이 아니며 본문의 별도 버전은 실패 카운터를 관측하지 않습니다.
7원래 main에서 1 뒤 2
13획득 없이 published 반환
21실선: 원래 코드의 분기와 반환
22원래 main은 실패 두 분기 미실행
23본문의 별도 버전은 실패 숫자를 안 읽음
원 도표의 설명과 주석
구분내용
설명원래 성공 경로 1/2와 미실행인 두 실패 분기를 구분합니다. 실패의 plain published 읽기는 정확한 스냅샷이 아니며 본문의 별도 버전은 실패 카운터를 관측하지 않습니다.
전체 읽기 정보미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 BoundedPublisher의 실제 두 성공 출력과 timed-out/interrupted 소스 분기를 나눕니다. 실패 분기의 plain published 무잠금 조회 결함은 그대로 존재합니다. 결과와 결함 · 미획득 읽기 미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 실패 상태를 반환해도 published를 읽는 경계가 안전해지는 것은 아닙니다. 미획득 두 분기의 카운터는 무잠금 조회입니다. published 카운터 변경과 해제 획득 뒤 증가 · finally에서 unlock 원문과 실행의 범위 main의 두 성공 결과: published=1 , published=2 timed-out 카운터 변경과 해제 증가와 unlock 없음 원문과 실행의 범위 미실행 분기 · published를 락 없이 읽어 반환 interrupted 카운터 변경과 해제 플래그 복원 · 증가와 unlock 없음 원문과 실행의 범위 미실행 분기 · published를 락 없이 읽어 반환 실패 결과의 published는 다른 스레드의 갱신을 동기화해 읽은 값이 아닙니다. 정확한 실패 스냅샷으로 해석할 수 없습니다. 성공 경로도 숫자 증가만 하며 실제 게시 I/O는 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 획득 시도

timed tryLock의 성공·false·중단 예외를 비교합니다.

  • 성공: 락 획득
  • 시간 만료: 미획득
  • 중단: 미획득 예외
  • 획득 여부가 보호된 접근과 finally 반납 책임을 정합니다.
각 상태 접근

성공만 락 안에서 published를 증가시킵니다.

  • 성공: published++, 보호된 읽기
  • false·예외: 증가 없음, plain published 읽기
  • 중단: 플래그 복원
  • 실패의 무잠금 숫자는 정확한 스냅샷이 아닙니다.
반환과 해제

각 분기의 결과와 해제 책임을 함께 둡니다.

  • 원 성공: 1 뒤 2, finally unlock
  • 미획득 분기: unlock 없음, plain 값 반환
  • 별도 개선 버전: 실패 카운터를 관측하지 않음
  • 단계는 세 결과를 차례로 실행하는 흐름이 아니라 같은 반환 경계 비교입니다.

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

성공 경로는 보호된 증가·반환, 실패 분기는 획득 없이 plain 카운터를 읽습니다. 분기별 획득 여부·갱신·읽기·반납을 단계로 설명하되 원 main은 성공만 실행함을 남깁니다.

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

BoundedPublisher의 실제 두 성공 출력과 timed-out/interrupted 소스 분기를 나눕니다. 실패 분기의 plain published 무잠금 조회 결함은 그대로 존재합니다.

결과와 결함 · 미획득 읽기

미획득 결과의 카운터 읽기는 락으로 보호되지 않는다

실패 상태를 반환해도 published를 읽는 경계가 안전해지는 것은 아닙니다. 미획득 두 분기의 카운터는 무잠금 조회입니다.

미획득 결과가 읽는 무잠금 카운터 원성공경로1/2와미실행두실패분기를구분합니다. 실패plainpublished읽기는정확한스냅샷이아니며본문별도버전은실패카운터를관측하지않습니다. 원 성공 경로 timed tryLock true 락 안에서 published++ 보호된 값 반환 원main에서1 뒤2 finally가 unlock 원 시간 만료 경로 timed tryLock false 증가·unlock 없음 plain 카운터를 읽음 획득 없이 published반환 정확한 스냅샷 아님 원 중단 경로 획득 중단 예외 플래그 복원 plain 카운터를 읽음 증가·unlock 없음 정확한 스냅샷 아님 실선: 원코드의 분기와 반환 원main은 실패 두 분기 미실행 본문별도버전은 실패숫자를 안 읽음
미획득 결과가 읽는 무잠금 카운터 원성공경로1/2와미실행두실패분기를구분합니다. 실패plainpublished읽기는정확한스냅샷이아니며본문별도버전은실패카운터를관측하지않습니다. 원 성공 경로 timed tryLock true 락 안에서 published++ 보호된 값 반환 원main에서1 뒤2 finally가 unlock 원 시간 만료 경로 timed tryLock false 증가·unlock 없음 plain 카운터를 읽음 획득 없이 published반환 정확한 스냅샷 아님 원 중단 경로 획득 중단 예외 플래그 복원 plain 카운터를 읽음 증가·unlock 없음 정확한 스냅샷 아님 실선: 원코드의 분기와 반환 원main은 실패 두 분기 미실행 본문별도버전은 실패숫자를 안 읽음

published

카운터 변경과 해제
획득 뒤 증가 · finally에서 unlock
원문과 실행의 범위
main의 두 성공 결과: published=1, published=2

timed-out

카운터 변경과 해제
증가와 unlock 없음
원문과 실행의 범위
미실행 분기 · published를 락 없이 읽어 반환

interrupted

카운터 변경과 해제
플래그 복원 · 증가와 unlock 없음
원문과 실행의 범위
미실행 분기 · published를 락 없이 읽어 반환

실패 결과의 published는 다른 스레드의 갱신을 동기화해 읽은 값이 아닙니다. 정확한 실패 스냅샷으로 해석할 수 없습니다. 성공 경로도 숫자 증가만 하며 실제 게시 I/O는 없습니다.

이 예의 조건

  • 원 main은 성공 반환 1·2만 실행합니다. 시간 만료·중단은 미실행 조건입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원래 성공 경로 1/2와 미실행인 두 실패 분기를 구분합니다. 실패의 plain published 읽기는 정확한 스냅샷이 아니며 본문의 별도 버전은 실패 카운터를 관측하지 않습니다.
7원래 main에서 1 뒤 2
13획득 없이 published 반환
21실선: 원래 코드의 분기와 반환
22원래 main은 실패 두 분기 미실행
23본문의 별도 버전은 실패 숫자를 안 읽음
원 도표의 설명과 주석
구분내용
설명원래 성공 경로 1/2와 미실행인 두 실패 분기를 구분합니다. 실패의 plain published 읽기는 정확한 스냅샷이 아니며 본문의 별도 버전은 실패 카운터를 관측하지 않습니다.
전체 읽기 정보미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 BoundedPublisher의 실제 두 성공 출력과 timed-out/interrupted 소스 분기를 나눕니다. 실패 분기의 plain published 무잠금 조회 결함은 그대로 존재합니다. 결과와 결함 · 미획득 읽기 미획득 결과의 카운터 읽기는 락으로 보호되지 않는다 실패 상태를 반환해도 published를 읽는 경계가 안전해지는 것은 아닙니다. 미획득 두 분기의 카운터는 무잠금 조회입니다. published 카운터 변경과 해제 획득 뒤 증가 · finally에서 unlock 원문과 실행의 범위 main의 두 성공 결과: published=1 , published=2 timed-out 카운터 변경과 해제 증가와 unlock 없음 원문과 실행의 범위 미실행 분기 · published를 락 없이 읽어 반환 interrupted 카운터 변경과 해제 플래그 복원 · 증가와 unlock 없음 원문과 실행의 범위 미실행 분기 · published를 락 없이 읽어 반환 실패 결과의 published는 다른 스레드의 갱신을 동기화해 읽은 값이 아닙니다. 정확한 실패 스냅샷으로 해석할 수 없습니다. 성공 경로도 숫자 증가만 하며 실제 게시 I/O는 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 획득 시도

timed tryLock의 성공·false·중단 예외를 비교합니다.

  • 성공: 락 획득
  • 시간 만료: 미획득
  • 중단: 미획득 예외
  • 획득 여부가 보호된 접근과 finally 반납 책임을 정합니다.
각 상태 접근

성공만 락 안에서 published를 증가시킵니다.

  • 성공: published++, 보호된 읽기
  • false·예외: 증가 없음, plain published 읽기
  • 중단: 플래그 복원
  • 실패의 무잠금 숫자는 정확한 스냅샷이 아닙니다.
반환과 해제

각 분기의 결과와 해제 책임을 함께 둡니다.

  • 원 성공: 1 뒤 2, finally unlock
  • 미획득 분기: unlock 없음, plain 값 반환
  • 별도 개선 버전: 실패 카운터를 관측하지 않음
  • 단계는 세 결과를 차례로 실행하는 흐름이 아니라 같은 반환 경계 비교입니다.

Duration.ZERO 시간 제한 형태는 즉시 시도와 비슷하지만 중단 가능·공정성 세부 규칙이 다를 수 있어 API 문서를 기준으로 합니다.

원 BoundedPublisher의 성공 경로는 published를 락 안에서 읽지만, timed-out과 interrupted는 획득하지 않고 같은 plain 필드를 읽습니다. 실패 결과의 숫자를 정확한 스냅샷이라고 해석할 수 없습니다. 실패한 획득에는 성공한 획득의 값 공개 규칙을 그대로 적용하지 않는다는 Lock의 메모리 규칙도 함께 확인하세요.

예시

다음 별도 버전은 미획득 결과에서 카운터를 관측하지 않습니다. null은 알려진 수량 0이 아니라 “이 시도에서 읽지 않음”입니다.

src/SafeBoundedPublisher.java
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public final class SafeBoundedPublisher {
    private record Outcome(String status, Integer observedPublished) {}
    private final ReentrantLock lock = new ReentrantLock();
    private int published;

    Outcome publish(Duration wait) {
        if (wait == null || wait.isNegative()) throw new IllegalArgumentException("wait");
        boolean acquired = false;
        try {
            acquired = lock.tryLock(wait.toNanos(), TimeUnit.NANOSECONDS);
            if (!acquired) return new Outcome("timed-out", null);
            published++;
            return new Outcome("published", published);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new Outcome("interrupted", null);
        } finally {
            if (acquired) lock.unlock();
        }
    }

    public static void main(String[] args) {
        SafeBoundedPublisher publisher = new SafeBoundedPublisher();
        System.out.println(publisher.publish(Duration.ofMillis(10)));
        Thread.currentThread().interrupt();
        System.out.println(publisher.publish(Duration.ZERO));
        System.out.println("flag-restored=" + Thread.currentThread().isInterrupted());
    }
}

첫 결과는 status=published, observedPublished=1이고 다음 결과는 status=interrupted, observedPublished=null입니다. 마지막 플래그는 true이며, 중단 요청이 이미 있는 시간 제한 획득은 빈 락에서도 예외 경로를 택합니다. 이 main은 timed-out 분기를 실행하지 않습니다. 실패 때 정확한 카운터가 꼭 필요하다면 별도 일관된 스냅샷 API와 그 대기·취소 예산을 정해야 합니다. 성공의 숫자 증가도 실제 게시 I/O 완료를 뜻하지 않습니다.


연습 문제

주요 계산을 보호하는 락을 최대 30ms 기다리고 얻으면 fresh, 얻지 못하면 cached, interrupt면 cancelled를 반환하는 메서드를 작성하세요.

성공한 획득만 해제해야 합니다.

정답과 세 결과

획득 여부를 나타내는 변수를 finally에서 확인하고 InterruptedException catch에서 플래그를 복원합니다.

exercise/TimedCacheFallbackSolution.java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public final class TimedCacheFallbackSolution {
    private final ReentrantLock lock = new ReentrantLock();

    String load() {
        boolean acquired = false;
        try {
            acquired = lock.tryLock(30, TimeUnit.MILLISECONDS);
            if (!acquired) return "cached";
            return "fresh";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "cancelled";
        } finally {
            if (acquired) lock.unlock();
        }
    }

    public static void main(String[] args) {
        TimedCacheFallbackSolution cache = new TimedCacheFallbackSolution();
        System.out.println(cache.load());
    }
}

main의 경합 없는 실행은 fresh입니다. 30ms는 획득 대기 요청값이며 전체 메서드 종료 기한은 아닙니다.

다른 스레드가 호출자의 대기 구간 내내 락을 유지하면 cached, 중단 가능 획득이 InterruptedException으로 끝나면 cancelled입니다. 제시한 main에서는 두 분기를 실행하지 않습니다.

tryLock은 “락을 얻지 못함”을 정상 제어 흐름으로 올려 줍니다.

반환값, 시간 제한, interrupt를 구분하고 획득에 성공한 경우에만 unlock하며 상위 기한과 대체 경로를 연결해야 무한 대기를 줄이는 목적을 달성합니다.