4주차 Unit 5.4 — tryLock()으로 데드락 회피

Psj·5일 전

F-lab

목록 보기
140/142

Unit 5.4 — tryLock()으로 데드락 회피

F-LAB JAVA · 4주차 · Phase 5 · 정교한 락: LockSupport와 ReentrantLock
★ 마스터 Unit (실무 직결) + 🏆 Phase 5 완주


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • tryLock() 의 두 형태 (즉시 / 타임아웃) 는?
  • 데드락 (Deadlock) 의 4가지 조건 은?
  • 데드락 회피 시나리오 (락1·락2 순환) 는?
  • lock() 이면 영원히 멈추는 이유는?
  • tryLock() 으로 데드락 회피 하는 원리는?
  • lock() 만 쓰는 시스템에서 데드락 회복 방법은?
  • tryLock(5, SECONDS) 실패 후 전략 은?
  • 락 순서 일관성 으로 데드락 방지는?
  • 실무의 데드락 진단/예방 은?

🎯 핵심 한 문장

tryLock() 은 락 획득을 시도하되 실패하면 즉시 (또는 타임아웃 후) 포기하는 메서드로, 무한 대기 대신 포기·재시도를 가능하게 하여 데드락을 회피한다.
boolean tryLock() 은 즉시 시도하여 실패 시 false 를 반환하고, boolean tryLock(time, unit) 은 지정 시간만큼 시도한 후 실패하면 false 를 반환한다.
데드락은 두 스레드가 서로 다른 순서로 두 락을 잡으려 할 때 (A 가 락1 보유·락2 대기, B 가 락2 보유·락1 대기) 발생하며, lock() 을 쓰면 둘 다 영원히 대기 하지만 tryLock() 을 쓰면 한 쪽이 포기하고 보유 락을 풀어 데드락을 회피 한다.
lock() 만 쓰는 시스템에서 데드락이 발생하면 회복 방법이 없어 재시작뿐 이므로, tryLock 으로 타임아웃·재시도를 두거나 모든 스레드가 락을 항상 같은 순서로 획득 하도록 (락 순서 일관성) 설계해야 한다.
tryLock(timeout) 실패 후에는 백오프 (backoff) 후 재시도, 대체 경로, 또는 작업 포기 등의 전략을 사용한다.

비유 — 좁은 다리에서 마주친 두 사람

데드락 = 좁은 외나무다리:

상황:
  - A 가 왼쪽에서, B 가 오른쪽에서
  - 다리 중간에서 마주침
  - 서로 비켜주길 기다림
  - 영원히 멈춤 (데드락)

lock() = 고집:
  - "당신이 비켜요"
  - 둘 다 안 비킴
  - 영원히 (회복 불가)

tryLock() = 양보:
  - "5초 기다려봤는데 안 되네"
  - 한 명이 뒤로 물러남 (포기)
  - 다시 시도 (재시도)
  - 데드락 회피

락 순서 일관성 = 규칙:
  - "항상 키 작은 사람이 먼저"
  - 순서 정하면 마주침 없음
  - 데드락 원천 차단

→ tryLock = 포기/재시도로 데드락 회피, 락 순서 일관 = 원천 방지.


🧭 9개 섹션 로드맵

1. tryLock()의 두 형태
2. 데드락의 4가지 조건
3. 데드락 시나리오
4. lock()이면 영원히 멈춤
5. tryLock()으로 회피
6. lock()만 쓰는 시스템의 회복
7. tryLock 실패 후 전략
8. 락 순서 일관성
9. 면접 + 자기 점검 + 마스터 50문항 + Phase 5 완주

1️⃣ tryLock()의 두 형태

1.1 두 형태

// 1. 즉시 시도
boolean tryLock();

// 2. 타임아웃 시도
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

1.2 즉시 tryLock()

// 즉시 시도 (실패 시 false)
if (lock.tryLock()) {
    try {
        // 락 획득 성공
        doWork();
    } finally {
        lock.unlock();
    }
} else {
    // 락 획득 실패 (즉시)
    log.info("이미 처리중인 작업이 있습니다.");
    // 대기 안 함
}

1.3 타임아웃 tryLock(time, unit)

// 타임아웃 시도
try {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            // 5초 내 획득 성공
            doWork();
        } finally {
            lock.unlock();
        }
    } else {
        // 5초 내 실패
        log.warn("락 획득 타임아웃");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

1.4 lock() 과 비교

lock vs tryLock:

lock():
  - 무한 대기
  - 반드시 획득 (또는 영원히)

tryLock():
  - 즉시 시도
  - 실패 시 false (대기 X)

tryLock(time):
  - 시간 내 시도
  - 실패 시 false

1.5 반환값 처리

// tryLock 반환값 반드시 확인
boolean acquired = lock.tryLock();
if (acquired) {
    try {
        // 임계 영역
    } finally {
        lock.unlock();   // 획득했을 때만
    }
}
// 획득 못 했으면 unlock 호출 X (예외)

1.6 ILIC 의 맥락

@Service
public class TryLockBasics {
    
    private final ReentrantLock lock = new ReentrantLock();
    
    // 즉시 시도 — 중복 처리 방지
    public boolean processIfAvailable(Shipment shipment) {
        if (lock.tryLock()) {   // 즉시
            try {
                doProcess(shipment);
                return true;
            } finally {
                lock.unlock();
            }
        }
        log.info("이미 처리중: {}", shipment.getId());
        return false;   // 다른 스레드가 처리 중
    }
    
    // 타임아웃 시도
    public boolean processWithTimeout(Shipment shipment) throws InterruptedException {
        if (lock.tryLock(5, TimeUnit.SECONDS)) {   // 5초
            try {
                doProcess(shipment);
                return true;
            } finally {
                lock.unlock();
            }
        }
        log.warn("처리 타임아웃: {}", shipment.getId());
        return false;
    }
    
    private void doProcess(Shipment s) { }
}

1.7 자기 점검 답변

tryLock()의 두 형태는?

:
1. 즉시 tryLock():

  • 시도, 실패 시 false
  • 대기 X
  1. 타임아웃 tryLock(time):

    • 시간 내 시도
    • 실패 시 false
  2. vs lock():

    • lock: 무한 대기
    • tryLock: 포기 가능
  3. 반환값:

    • 확인 후 unlock

2️⃣ 데드락의 4가지 조건

2.1 4가지 조건

데드락 4가지 조건 (모두 충족 시):

1. 상호 배제 (Mutual Exclusion)
   - 자원을 한 번에 하나만

2. 점유 대기 (Hold and Wait)
   - 자원 보유한 채 다른 자원 대기

3. 비선점 (No Preemption)
   - 자원 강제 회수 불가

4. 순환 대기 (Circular Wait)
   - 서로 순환적으로 대기

2.2 각 조건

상호 배제:
  - synchronized, lock
  - 한 스레드만 임계 영역

점유 대기:
  - 락A 보유 + 락B 대기

비선점:
  - 락 강제로 뺏기 불가
  - 스스로 풀어야

순환 대기:
  - A→B→A 순환
  - 서로 기다림

2.3 데드락 방지 = 조건 깨기

데드락 방지 — 조건 하나 깨기:

1. 상호 배제 깨기:
   - 어려움 (락의 본질)

2. 점유 대기 깨기:
   - 모든 락 한 번에 (전부 또는 포기)

3. 비선점 깨기:
   - tryLock (타임아웃 → 포기)

4. 순환 대기 깨기:
   - 락 순서 일관 (가장 실용적)

2.4 시각화

데드락 4조건:

스레드 A: 락1 보유 ──점유대기──→ 락2 대기
                                    ↑
                                  순환대기
                                    ↓
스레드 B: 락2 보유 ──점유대기──→ 락1 대기

  상호 배제 + 점유 대기 + 비선점 + 순환 대기
  → 데드락

2.5 ILIC 의 맥락

// 데드락 4조건이 충족되는 예
public class DeadlockConditions {
    
    private final ReentrantLock lockA = new ReentrantLock();
    private final ReentrantLock lockB = new ReentrantLock();
    
    // 스레드 1
    public void method1() {
        lockA.lock();   // 1. 상호 배제 (A)
        try {
            // 2. 점유 대기 (A 보유한 채)
            lockB.lock();   // B 대기
            try {
                // ...
            } finally {
                lockB.unlock();
            }
        } finally {
            lockA.unlock();   // 3. 비선점 (스스로만)
        }
    }
    
    // 스레드 2 (반대 순서)
    public void method2() {
        lockB.lock();   // B 먼저
        try {
            lockA.lock();   // A 대기 (4. 순환 대기)
            try {
                // ...
            } finally {
                lockA.unlock();
            }
        } finally {
            lockB.unlock();
        }
    }
    // method1 + method2 동시 → 데드락
}

2.6 자기 점검 답변

데드락의 4가지 조건은?

:
1. 4가지:

  • 상호 배제
  • 점유 대기
  • 비선점
  • 순환 대기
  1. 모두 충족:

    • 4개 모두 시 데드락
  2. 방지:

    • 하나 깨기
    • 순환 대기 깨기 (실용)
  3. tryLock:

    • 비선점 깨기 (포기)

3️⃣ 데드락 시나리오

3.1 고전 시나리오

계좌 이체 데드락:

스레드 1: transfer(accA, accB)
  - accA 락 획득
  - accB 락 대기

스레드 2: transfer(accB, accA)
  - accB 락 획득
  - accA 락 대기

→ 서로 대기 → 데드락

3.2 코드

public class TransferDeadlock {
    
    static class Account {
        final ReentrantLock lock = new ReentrantLock();
        int balance;
    }
    
    // ❌ 데드락 위험
    public void transfer(Account from, Account to, int amount) {
        from.lock.lock();   // from 먼저
        try {
            to.lock.lock();   // to 대기
            try {
                from.balance -= amount;
                to.balance += amount;
            } finally {
                to.lock.unlock();
            }
        } finally {
            from.lock.unlock();
        }
    }
    
    // 데드락 발생:
    // 스레드 1: transfer(accA, accB) → accA → accB
    // 스레드 2: transfer(accB, accA) → accB → accA
    // → 순환 대기
}

3.3 시각화

이체 데드락:

스레드 1: transfer(A, B)
  A.lock() 성공
  B.lock() 대기 ───┐
                    │ (B 는 스레드 2 보유)
스레드 2: transfer(B, A)
  B.lock() 성공
  A.lock() 대기 ───┘
                    (A 는 스레드 1 보유)

  서로 대기 → 영원히

3.4 발생 타이밍

데드락 발생 타이밍:

  타이밍 의존 (비결정적):
    - 동시에 두 transfer
    - 각자 첫 락 획득
    - 두 번째 락 대기

  가끔 발생:
    - 운 나쁘면 데드락
    - 재현 어려움
    - 운영 중 갑자기

3.5 ILIC 의 맥락

@Service
public class ShipmentTransferDeadlock {
    
    // ❌ 데드락 위험 — 계정 간 잔액 이체
    public void transferBalance(ShipmentAccount from, ShipmentAccount to, 
                                 BigDecimal amount) {
        from.lock.lock();
        try {
            to.lock.lock();   // 순서 불일치 시 데드락
            try {
                from.balance = from.balance.subtract(amount);
                to.balance = to.balance.add(amount);
            } finally {
                to.lock.unlock();
            }
        } finally {
            from.lock.unlock();
        }
    }
    
    // transferBalance(accA, accB) 와
    // transferBalance(accB, accA) 동시 → 데드락
    
    static class ShipmentAccount {
        final ReentrantLock lock = new ReentrantLock();
        BigDecimal balance = BigDecimal.ZERO;
    }
}

3.6 자기 점검 답변

데드락 시나리오는?

:
1. 시나리오:

  • A 가 락1 보유 + 락2 대기
  • B 가 락2 보유 + 락1 대기
  1. 이체 예:

    • transfer(A,B) + transfer(B,A)
    • 순서 불일치
  2. 타이밍:

    • 비결정적
    • 가끔 발생
  3. 결과:

    • 서로 대기
    • 영원히

4️⃣ lock()이면 영원히 멈춤

4.1 lock() 의 무한 대기

lock() 데드락:

  두 스레드 모두 lock() 사용:
    - 첫 락 획득
    - 두 번째 락 무한 대기

  → 서로 대기
  → 영원히 멈춤
  → 회복 불가

4.2 회복 불가

lock() 데드락 회복 불가:

  - 타임아웃 X (무한 대기)
  - 인터럽트 X (lock() 은)
  - 자원 강제 회수 X (비선점)

  → 회복 방법 없음
  → 재시작뿐

4.3 시각화

lock() 데드락:

스레드 1: A.lock() 성공
          B.lock() [무한 대기──────────]
                              ↑ 영원히
스레드 2: B.lock() 성공
          A.lock() [무한 대기──────────]
                              ↑ 영원히

  → 둘 다 영원히
  → 회복 불가

4.4 운영 영향

운영 영향:

  - 두 스레드 영원히 점유
  - 관련 자원 락
  - 스레드 풀 고갈 가능
  - 서비스 장애

진단:
  - jstack → "deadlock" 감지
  - 하지만 회복 불가 (재시작)

4.5 jstack 데드락 감지

$ jstack <pid>

# 데드락 감지:
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor ... (Account B)
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor ... (Account A)
  which is held by "Thread-1"

# jstack 이 데드락 알려줌
# 하지만 lock() 은 회복 불가

4.6 ILIC 의 맥락

@Service
public class LockDeadlockProblem {
    
    private final ReentrantLock lockA = new ReentrantLock();
    private final ReentrantLock lockB = new ReentrantLock();
    
    // ❌ lock() — 데드락 시 회복 불가
    public void process1() {
        lockA.lock();
        try {
            sleep(100);
            lockB.lock();   // 무한 대기 (데드락 시)
            try {
                doWork();
            } finally {
                lockB.unlock();
            }
        } finally {
            lockA.unlock();
        }
    }
    
    public void process2() {
        lockB.lock();   // 반대 순서
        try {
            sleep(100);
            lockA.lock();   // 무한 대기
            try {
                doWork();
            } finally {
                lockA.unlock();
            }
        } finally {
            lockB.unlock();
        }
    }
    // 동시 호출 시 데드락 → 재시작뿐
    
    private void doWork() { }
    private void sleep(long ms) { try { Thread.sleep(ms); } catch (Exception e) {} }
}

4.7 자기 점검 답변

lock()이면 영원히 멈추는 이유는?

:
1. 무한 대기:

  • lock() 은 타임아웃 X
  • 둘 다 두 번째 락 대기
  1. 회복 불가:

    • 타임아웃/인터럽트 X
    • 강제 회수 X
  2. 운영 영향:

    • 스레드 점유
    • 서비스 장애
  3. 진단:

    • jstack (감지)
    • 하지만 재시작뿐

5️⃣ tryLock()으로 회피

5.1 회피 원리

tryLock 데드락 회피:

  두 번째 락을 tryLock 으로:
    - 실패 시 첫 락도 풀기
    - 재시도

  → 한 쪽이 양보
  → 순환 대기 깨짐
  → 데드락 회피

5.2 코드

// ✓ tryLock 으로 데드락 회피
public boolean transfer(Account from, Account to, int amount) 
        throws InterruptedException {
    while (true) {
        if (from.lock.tryLock()) {   // 첫 락 시도
            try {
                if (to.lock.tryLock()) {   // 두 번째 시도
                    try {
                        from.balance -= amount;
                        to.balance += amount;
                        return true;   // 성공
                    } finally {
                        to.lock.unlock();
                    }
                }
                // to 실패 → from 도 풀고 재시도
            } finally {
                from.lock.unlock();
            }
        }
        // 둘 다 못 얻음 → 잠시 후 재시도
        Thread.sleep(10);   // 백오프
    }
}

5.3 회피 메커니즘

회피 메커니즘:

스레드 1: from.tryLock() 성공
          to.tryLock() 실패
          → from 풀기 (양보)
          → 재시도

스레드 2: 그 사이 to, from 획득
          → 처리 완료

  한 쪽 양보 → 데드락 깨짐

5.4 시각화

tryLock 회피:

스레드 1: A.tryLock() 성공
          B.tryLock() 실패 → A 풀기 (양보)
          [잠시 대기]
          재시도...

스레드 2: B 보유
          A.tryLock() 성공 (1 이 풀었으니)
          처리 완료
          B, A 풀기

스레드 1: 재시도 → A, B 획득 → 완료

  → 데드락 회피 (양보 + 재시도)

5.5 타임아웃 버전

// 타임아웃 tryLock 회피
public boolean transferWithTimeout(Account from, Account to, int amount) 
        throws InterruptedException {
    long timeout = 1000;
    if (from.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
        try {
            if (to.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
                try {
                    from.balance -= amount;
                    to.balance += amount;
                    return true;
                } finally {
                    to.lock.unlock();
                }
            }
        } finally {
            from.lock.unlock();
        }
    }
    return false;   // 타임아웃 → 포기 (데드락 회피)
}

5.6 ILIC 의 맥락

@Service
public class TryLockDeadlockAvoidance {
    
    // ✓ tryLock — 데드락 회피
    public boolean transferBalance(ShipmentAccount from, ShipmentAccount to,
                                    BigDecimal amount) throws InterruptedException {
        int retries = 0;
        while (retries < 10) {
            if (from.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (to.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            from.balance = from.balance.subtract(amount);
                            to.balance = to.balance.add(amount);
                            return true;
                        } finally {
                            to.lock.unlock();
                        }
                    }
                    // to 실패 → from 풀고 재시도
                } finally {
                    from.lock.unlock();
                }
            }
            retries++;
            Thread.sleep(ThreadLocalRandom.current().nextInt(50));   // 랜덤 백오프
        }
        log.warn("이체 실패 (재시도 초과)");
        return false;
    }
    
    static class ShipmentAccount {
        final ReentrantLock lock = new ReentrantLock();
        BigDecimal balance = BigDecimal.ZERO;
    }
}

5.7 자기 점검 답변

tryLock()으로 회피하는 원리는?

:
1. 원리:

  • 두 번째 락 tryLock
  • 실패 시 첫 락 풀기
  • 재시도
  1. 메커니즘:

    • 한 쪽 양보
    • 순환 대기 깨짐
  2. 타임아웃:

    • tryLock(time)
    • 포기
  3. 재시도:

    • 백오프 (랜덤)

6️⃣ lock()만 쓰는 시스템의 회복

6.1 회복 방법

lock() 만 쓰는 시스템 데드락 회복:

  근본적으로 회복 불가:
    - 타임아웃 X
    - 인터럽트 X (lock())

  유일한 방법:
    - 프로세스 재시작

6.2 재시작뿐

회복 = 재시작:

  데드락 발생:
    - 두 스레드 영원히
    - 자원 점유

  회복:
    - 서버 재시작
    - 데드락 스레드 강제 종료
    - (다른 방법 없음)

  비용:
    - 다운타임
    - 진행 중 작업 손실

6.3 사전 방지가 답

사후 회복 불가 → 사전 방지:

  lock() 데드락은 회복 불가하므로
  사전 방지가 유일한 해결.

방지:
  1. 락 순서 일관 (섹션 8)
  2. tryLock 사용 (타임아웃)
  3. 락 최소화
  4. 단일 락

6.4 모니터링

// 데드락 감지 (ThreadMXBean)
public class DeadlockDetector {
    
    public void detectDeadlock() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] deadlocked = bean.findDeadlockedThreads();
        
        if (deadlocked != null) {
            log.error("데드락 감지! {} 스레드", deadlocked.length);
            
            ThreadInfo[] infos = bean.getThreadInfo(deadlocked);
            for (ThreadInfo info : infos) {
                log.error("Deadlocked: {}", info.getThreadName());
            }
            
            // 알림 발송 → 운영자가 재시작 판단
            // (자동 회복 불가)
        }
    }
}

6.5 변환 — lock → tryLock

// ❌ lock() (회복 불가)
public void process() {
    lockA.lock();
    try {
        lockB.lock();   // 데드락 시 무한
        try { } finally { lockB.unlock(); }
    } finally { lockA.unlock(); }
}

// ✓ tryLock (회피 가능)
public boolean processSafe() throws InterruptedException {
    if (lockA.tryLock(1, TimeUnit.SECONDS)) {
        try {
            if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                try { return true; }
                finally { lockB.unlock(); }
            }
        } finally { lockA.unlock(); }
    }
    return false;   // 포기 (회복)
}

6.6 ILIC 의 맥락

@Service
public class DeadlockRecoveryStrategy {
    
    // 데드락 감지 + 알림 (회복은 재시작)
    @Scheduled(fixedRate = 30000)
    public void monitorDeadlock() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] deadlocked = bean.findDeadlockedThreads();
        
        if (deadlocked != null) {
            alertService.sendCritical("데드락 감지! 재시작 필요");
            // lock() 데드락은 자동 회복 불가
            // → 운영자 개입 (재시작)
        }
    }
    
    // 예방 — tryLock 으로 전환
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();
    
    public boolean safeProcess() throws InterruptedException {
        if (lock1.tryLock(1, TimeUnit.SECONDS)) {
            try {
                if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        doWork();
                        return true;
                    } finally {
                        lock2.unlock();
                    }
                }
            } finally {
                lock1.unlock();
            }
        }
        return false;   // 데드락 회피
    }
    
    private void doWork() { }
}

6.7 자기 점검 답변

lock()만 쓰는 시스템에서 데드락 회복 방법은?

:
1. 회복 불가:

  • 타임아웃/인터럽트 X
  • 재시작뿐
  1. 재시작:

    • 강제 종료
    • 다운타임
  2. 사전 방지:

    • 락 순서 일관
    • tryLock
  3. 모니터링:

    • ThreadMXBean
    • 감지 → 알림

7️⃣ tryLock 실패 후 전략

7.1 실패 후 전략들

tryLock(5, SECONDS) 실패 후:

1. 재시도 (백오프)
   - 잠시 후 다시
   - 랜덤 백오프

2. 대체 경로
   - 다른 방법으로

3. 작업 포기
   - 사용자에게 알림
   - 나중에

4. 큐에 적재
   - 비동기 처리

7.2 백오프 재시도

// 백오프 재시도
public boolean processWithRetry(Shipment shipment) throws InterruptedException {
    int maxRetries = 5;
    long backoff = 100;
    
    for (int i = 0; i < maxRetries; i++) {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                doProcess(shipment);
                return true;
            } finally {
                lock.unlock();
            }
        }
        // 실패 → 백오프 후 재시도
        Thread.sleep(backoff + ThreadLocalRandom.current().nextLong(50));
        backoff *= 2;   // 지수 백오프
    }
    return false;   // 최종 실패
}

7.3 지수 백오프 + 지터

지수 백오프 (Exponential Backoff):

  재시도마다 대기 시간 증가:
    - 1차: 100ms
    - 2차: 200ms
    - 3차: 400ms
    - ...

지터 (Jitter):
  - 랜덤 추가
  - 동시 재시도 분산
  - 충돌 방지

backoff = base * 2^retry + random

7.4 대체 경로

// 대체 경로
public Result process(Shipment shipment) throws InterruptedException {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            return processFast(shipment);   // 빠른 경로
        } finally {
            lock.unlock();
        }
    }
    // 락 못 얻음 → 대체 경로
    return processSlow(shipment);   // 락 없는 느린 경로
}

private Result processFast(Shipment s) { return null; }
private Result processSlow(Shipment s) { return null; }

7.5 큐 적재

// 큐에 적재 (비동기)
public void process(Shipment shipment) throws InterruptedException {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            doProcess(shipment);
        } finally {
            lock.unlock();
        }
    } else {
        // 락 못 얻음 → 큐에 적재
        deferredQueue.offer(shipment);
        log.info("나중 처리 큐에 적재: {}", shipment.getId());
    }
}

7.6 ILIC 의 맥락

@Service
public class TryLockFailureStrategy {
    
    private final ReentrantLock lock = new ReentrantLock();
    private final BlockingQueue<Shipment> deferredQueue = new LinkedBlockingQueue<>();
    
    public ProcessResult process(Shipment shipment) throws InterruptedException {
        // 1. 즉시 시도 + 백오프 재시도
        long backoff = 100;
        for (int i = 0; i < 3; i++) {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    doProcess(shipment);
                    return ProcessResult.success();
                } finally {
                    lock.unlock();
                }
            }
            Thread.sleep(backoff + ThreadLocalRandom.current().nextLong(50));
            backoff *= 2;   // 지수 백오프
        }
        
        // 2. 재시도 실패 → 큐 적재 (대체 전략)
        deferredQueue.offer(shipment);
        log.warn("처리 지연: {} (큐 적재)", shipment.getId());
        return ProcessResult.deferred();
    }
    
    private void doProcess(Shipment s) { }
    
    record ProcessResult(String status) {
        static ProcessResult success() { return new ProcessResult("SUCCESS"); }
        static ProcessResult deferred() { return new ProcessResult("DEFERRED"); }
    }
}

7.7 자기 점검 답변

tryLock(5, SECONDS) 실패 후 전략은?

:
1. 재시도:

  • 백오프
  • 지수 백오프 + 지터
  1. 대체 경로:

    • 락 없는 방법
  2. 작업 포기:

    • 알림
  3. 큐 적재:

    • 비동기 처리
    • 나중에

8️⃣ 락 순서 일관성

8.1 순환 대기 깨기

락 순서 일관성:

  모든 스레드가 락을 항상 같은 순서로 획득.

효과:
  - 순환 대기 불가
  - 데드락 원천 방지

가장 실용적인 방법

8.2 순서 정하기

// 락에 순서 부여 (예: id)
public void transferOrdered(Account from, Account to, int amount) {
    // id 작은 것 먼저 (일관된 순서)
    Account first = from.id < to.id ? from : to;
    Account second = from.id < to.id ? to : from;
    
    first.lock.lock();   // 항상 작은 id 먼저
    try {
        second.lock.lock();
        try {
            from.balance -= amount;
            to.balance += amount;
        } finally {
            second.lock.unlock();
        }
    } finally {
        first.lock.unlock();
    }
}
// 모든 스레드가 id 순서 → 순환 X → 데드락 X

8.3 왜 효과적

순서 일관성이 효과적인 이유:

  순환 대기 (조건 4) 를 깸:
    - 모두 같은 순서
    - A→B 만 (B→A 없음)
    - 순환 불가

예:
  스레드 1: A→B
  스레드 2: A→B (같은 순서)
  → 순환 X
  → 데드락 X

8.4 시각화

순서 일관:

스레드 1: A.lock()→B.lock()  (A 먼저)
스레드 2: A.lock()→B.lock()  (A 먼저, 같음)

  - 스레드 1 이 A 보유 시
  - 스레드 2 는 A 대기 (B 안 잡음)
  - 1 이 A,B 처리 후 풀면
  - 2 가 진행
  → 순환 X → 데드락 X

8.5 해시 기반 순서

// id 없으면 해시 기반
public void transfer(Object lockA, Object lockB) {
    int hashA = System.identityHashCode(lockA);
    int hashB = System.identityHashCode(lockB);
    
    Object first = hashA < hashB ? lockA : lockB;
    Object second = hashA < hashB ? lockB : lockA;
    
    synchronized (first) {
        synchronized (second) {
            // 일관된 순서
        }
    }
    // 해시 같으면 (드물게) 타이브레이커 락 추가
}

8.6 ILIC 의 맥락

@Service
public class LockOrderingConsistency {
    
    // ✓ 락 순서 일관 (id 기반)
    public void transferBalance(ShipmentAccount from, ShipmentAccount to,
                                 BigDecimal amount) {
        // 항상 id 작은 것 먼저 (순환 방지)
        ShipmentAccount first = from.id < to.id ? from : to;
        ShipmentAccount second = from.id < to.id ? to : from;
        
        first.lock.lock();
        try {
            second.lock.lock();
            try {
                from.balance = from.balance.subtract(amount);
                to.balance = to.balance.add(amount);
            } finally {
                second.lock.unlock();
            }
        } finally {
            first.lock.unlock();
        }
        // 모든 스레드 같은 순서 → 데드락 X
    }
    
    static class ShipmentAccount {
        final long id;
        final ReentrantLock lock = new ReentrantLock();
        BigDecimal balance = BigDecimal.ZERO;
        
        ShipmentAccount(long id) { this.id = id; }
    }
}

8.7 자기 점검 답변

락 순서 일관성으로 데드락 방지는?

:
1. 순서 일관:

  • 항상 같은 순서
  • 순환 대기 깸
  1. 방법:

    • id 순서
    • 해시 기반
  2. 효과:

    • 순환 불가
    • 원천 방지
  3. 실용성:

    • 가장 실용적
    • tryLock 보다 근본적

9️⃣ 면접 + 자기 점검 + 마스터 50문항 + Phase 5 완주

9.1 면접 단골 질문 매핑

Q핵심 답변
tryLock 두 형태?즉시 / 타임아웃
데드락 4조건?상호배제/점유대기/비선점/순환대기
데드락 시나리오?락 순서 불일치
lock() 데드락?영원히 (회복 불가)
tryLock 회피?포기/재시도
회복 방법?재시작 (lock())
실패 후 전략?백오프, 큐
락 순서 일관?순환 대기 깸
백오프?지수 + 지터
데드락 진단?jstack, ThreadMXBean

9.2 마스터 자기 점검 체크리스트

tryLock

  • 두 형태
  • 즉시/타임아웃
  • 반환값

데드락 조건

  • 4가지
  • 깨기

시나리오

  • 순서 불일치
  • 타이밍

lock 데드락

  • 영원히
  • 회복 불가

tryLock 회피

  • 양보/재시도
  • 메커니즘

회복

  • 재시작
  • 사전 방지

실패 전략

  • 백오프

락 순서

  • 일관성
  • 순환 깸

9.3 tryLock/데드락 마스터 50문항

tryLock (12문항)

Q1. tryLock()? → 즉시 시도
Q2. tryLock(time)? → 타임아웃
Q3. 실패 시? → false
Q4. lock 과 차이? → 무한 vs 포기
Q5. 반환값? → boolean
Q6. 획득 후? → try-finally
Q7. 실패 후 unlock? → 안 함
Q8. InterruptedException? → tryLock(time)
Q9. 즉시 용도? → 중복 방지
Q10. 타임아웃 용도? → 데드락 회피
Q11. 백오프? → 재시도 간격
Q12. 지터? → 랜덤 분산

데드락 조건 (13문항)

Q13. 4조건? → 상호배제/점유대기/비선점/순환대기
Q14. 상호 배제? → 한 번에 하나
Q15. 점유 대기? → 보유한 채 대기
Q16. 비선점? → 강제 회수 X
Q17. 순환 대기? → 서로 순환
Q18. 모두 충족? → 데드락
Q19. 방지? → 하나 깨기
Q20. 순환 깨기? → 락 순서
Q21. 비선점 깨기? → tryLock
Q22. 점유대기 깨기? → 전부 또는 포기
Q23. 가장 실용? → 순서 일관
Q24. 데드락 정의? → 서로 대기
Q25. 라이브락? → 계속 양보 (다름)

lock/tryLock 회피 (13문항)

Q26. lock 데드락? → 영원히
Q27. 회복? → 재시작
Q28. tryLock 회피? → 양보/재시도
Q29. 양보? → 첫 락 풀기
Q30. 재시도? → 백오프
Q31. 타임아웃 회피? → tryLock(time)
Q32. 순환 깸? → 양보로
Q33. 회복 불가? → lock()
Q34. 회복 가능? → tryLock
Q35. 사전 방지? → 순서, tryLock
Q36. 진단? → jstack
Q37. ThreadMXBean? → findDeadlockedThreads
Q38. 감지 후? → 알림/재시작

전략/순서 (12문항)

Q39. 실패 전략? → 백오프, 큐
Q40. 지수 백오프? → 2배씩
Q41. 지터? → 랜덤
Q42. 대체 경로? → 락 없는 방법
Q43. 큐 적재? → 비동기
Q44. 락 순서? → 일관성
Q45. 순서 방법? → id, 해시
Q46. 순서 효과? → 순환 X
Q47. id 순서? → 작은 것 먼저
Q48. 해시 순서? → identityHashCode
Q49. 순서 vs tryLock? → 근본 vs 회피
Q50. 권장? → 순서 + tryLock

9.4 채점

50 / 50 → tryLock/데드락 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 5.4 재학습

9.5 Phase 5 완주 정리

Phase 5 — 정교한 락

Unit 5.1 — synchronized의 한계
  - 무한 대기, 인터럽트 X, 공정성 X

Unit 5.2 — LockSupport
  - park/unpark
  - 저수준, 고수준 기반

Unit 5.3 — ReentrantLock
  - lock/unlock, try-finally
  - 재진입, 공정성, Condition

Unit 5.4 — tryLock (★ 마스터)
  - 데드락 회피
  - 락 순서 일관

9.6 Phase 5 핵심 통찰

Phase 5 핵심 통찰 5가지:

1. synchronized 한계
   - 무한 대기, 인터럽트 X

2. ReentrantLock
   - 정교한 제어
   - try-finally 필수

3. tryLock
   - 포기/재시도
   - 데드락 회피

4. 데드락 4조건
   - 순환 대기 깨기 (락 순서)

5. 실무
   - 락 순서 일관 + tryLock

9.7 4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
✅ Phase 4 — synchronized & volatile (5 Unit) ★ 1차 정점
✅ Phase 5 — Lock 도구 (4 Unit) ← 완주
⏭ Phase 6 — 스레드 협력 (4 Unit)
⏭ Phase 7 — Executor (7 Unit) ★ 2차 정점
⏭ Phase 8 — 고급 비동기 (3 Unit)

총: 21/35 Unit (Phase 5 완주, 약 60%)

9.8 추가 심화 질문

Q1: 라이브락 (Livelock)?

답:

  • 데드락과 달리 계속 활동
  • 서로 양보만 반복
  • 진전 없음
  • tryLock 회피 시 가능
  • 랜덤 백오프로 완화

Q2: 기아 (Starvation) vs 데드락?

답:

  • 데드락: 서로 대기 (멈춤)
  • 기아: 특정 스레드 자원 못 받음
  • 데드락은 멈춤, 기아는 지연
  • 공정성으로 기아 완화

Q3: 데드락 예방 vs 회피 vs 감지?

답:

  • 예방: 조건 깨기 (사전)
  • 회피: 안전 상태 유지 (은행원 알고리즘)
  • 감지: 발생 후 탐지 + 회복
  • 자바: 주로 예방 (순서)

Q4: tryLock 의 공정성?

답:

  • tryLock() 은 공정성 무시 (즉시)
  • 공정 락이어도 새치기
  • tryLock(time) 은 공정성 따름
  • 주의 필요

Q5: 분산 환경 데드락?

답:

  • 여러 노드 간 락
  • 분산 락 (Redis, Zookeeper)
  • 타임아웃 필수
  • 데드락 더 복잡

🎯 핵심 요약 — 3줄 정리

1. tryLock

  • 즉시 / 타임아웃 시도
  • 실패 시 포기 (false)

2. 데드락

  • 4조건 (상호배제/점유대기/비선점/순환대기)
  • lock(): 영원히 (회복 불가)
  • tryLock(): 포기/재시도 (회피)

3. 방지

  • 락 순서 일관 (순환 깸, 근본)
  • tryLock + 백오프 (회피)

🏆 Phase 5 완주 — Lock 도구 마스터

🚀 Phase 5 — 정교한 락
  ✅ Unit 5.1 synchronized의 한계
  ✅ Unit 5.2 LockSupport
  ✅ Unit 5.3 ReentrantLock
  ✅ Unit 5.4 tryLock (★ 마스터) ← 여기, Phase 5 완주

→ synchronized 한계 → ReentrantLock
→ tryLock 데드락 회피
→ 락 순서 일관성

📚 다음으로...

Phase 6 — 스레드 간 협력 (생산자-소비자, 인터럽트, yield)

Phase 6 — 스레드 협력 (4 Unit)

Unit 6.1 — 생산자-소비자 문제
Unit 6.2 — wait()과 notify()
Unit 6.3 — 인터럽트(Interrupt) 메커니즘
Unit 6.4 — yield()

4주차 누적 진행

✅ Phase 1~5 (21 Unit, 1차 정점 완료)
⏭ Phase 6 — 스레드 협력 (4 Unit)

총: 21/35 Unit (약 60%)

★ 마스터 Unit — tryLock 데드락 회피 완료
🏆 Phase 5 완주 — Lock 도구 마스터

profile
Software Developer

0개의 댓글