F-LAB JAVA · 4주차 · Phase 5 · 정교한 락: LockSupport와 ReentrantLock
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
ReentrantLock 은
java.util.concurrent.locks.Lock인터페이스의 대표 구현체로, synchronized 와 같은 상호 배제 효과에 더해 타임아웃·인터럽트·공정성 등 정교한 제어를 제공한다.
lock()으로 락을 획득하고unlock()으로 반납하며, 반드시try-finally로 감싸 finally 에서 unlock 을 호출 해야 한다 — 임계 영역 중간에 예외나 return 이 발생해도 락이 확실히 반납되도록 하기 위함이다.
finally 에서 unlock 을 깜박하면 락이 영원히 반납되지 않아 다른 스레드들이 무한 대기하는 데드락 이 발생한다 (synchronized 의 자동 반환과 가장 큰 차이).
ReentrantLock 은 이름대로 재진입 가능 하며 (같은 스레드가 락 재획득, 카운트 관리), 생성자에true를 주면 공정 모드 (FIFO) 로 동작한다.
Condition객체로 wait/notify 보다 정교한 조건별 대기/통지가 가능하며 (여러 Condition), 이것이 ReentrantLock 이 "실무 표준" 으로 불리는 이유다.
ReentrantLock = 수동 잠금 금고:
synchronized = 자동문 (자동 잠금/해제):
- 들어가면 자동 잠김
- 나오면 자동 해제
ReentrantLock = 수동 금고 (직접 열쇠):
lock() = 금고 열기 (직접)
unlock() = 금고 닫기 (직접, 필수!)
try-finally 필수:
try {
금고 사용
} finally {
금고 닫기 (unlock) // 예외 나도 닫기
}
unlock 깜박:
- 금고 안 닫음
- 다른 사람 영원히 못 씀 (데드락)
추가 기능 (자동문엔 없음):
- tryLock: "잠겨있으면 포기"
- lockInterruptibly: "두드리면 나옴"
- 공정 모드: "줄 순서"
- Condition: "특정 신호 대기"
→ ReentrantLock = 수동 락 (강력하지만 try-finally 필수).
1. ReentrantLock의 정의
2. lock() / unlock() 사용
3. try-finally 필수
4. unlock 누락의 사고
5. lock() vs synchronized
6. 재진입성
7. 공정성 옵션
8. Condition (정교한 대기/통지)
9. 면접 + 자기 점검
ReentrantLock:
java.util.concurrent.locks.ReentrantLock
Lock 인터페이스의 대표 구현체.
특징:
- synchronized 효과 + 추가 기능
- 명시적 lock/unlock
- 재진입 가능
- 공정성 옵션
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
// ReentrantLock 이 구현
import java.util.concurrent.locks.ReentrantLock;
// 비공정 (기본)
ReentrantLock lock = new ReentrantLock();
// 공정 모드
ReentrantLock fairLock = new ReentrantLock(true);
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 획득
try {
count++;
} finally {
lock.unlock(); // 반납 (필수)
}
}
}
@Service
public class ShipmentLockService {
private int processedCount = 0;
private final ReentrantLock lock = new ReentrantLock();
public void process(Shipment shipment) {
lock.lock();
try {
processedCount++;
doProcess(shipment);
} finally {
lock.unlock();
}
}
private void doProcess(Shipment s) { }
}
ReentrantLock의 정의는?
답:
1. 정의:
특징:
생성:
사용:
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 1. 락 획득
try {
// 2. 임계 영역
} finally {
lock.unlock(); // 3. 락 반납 (finally)
}
lock():
락 획득.
- 락 비어있으면 → 획득 (즉시)
- 락 있으면 → 대기 (WAITING)
특징:
- 무한 대기 (lock())
- 인터럽트 X (lock())
unlock():
락 반납.
- 락 보유 스레드만 호출 가능
- 재진입 카운트 감소
- 카운트 0 시 완전 반납
주의:
- 락 없이 unlock → IllegalMonitorStateException
// ❌ try-finally 없음
lock.lock();
count++; // 예외 시 unlock 안 됨
lock.unlock();
// 예외 발생 시 락 영원히 안 풀림
// ❌ unlock 위치 잘못
lock.lock();
try {
count++;
lock.unlock(); // try 안에서 (잘못)
moreWork(); // 이미 unlock (위험)
} finally {
// 비어있음
}
// ✓ 올바른 패턴
lock.lock();
try {
count++;
// 모든 임계 영역
} finally {
lock.unlock(); // 항상 finally
}
@Service
public class LockUsageExample {
private final ReentrantLock lock = new ReentrantLock();
private BigDecimal balance = BigDecimal.ZERO;
public void deposit(BigDecimal amount) {
lock.lock();
try {
balance = balance.add(amount);
} finally {
lock.unlock();
}
}
public BigDecimal withdraw(BigDecimal amount) {
lock.lock();
try {
if (balance.compareTo(amount) >= 0) {
balance = balance.subtract(amount);
return amount;
}
return BigDecimal.ZERO;
} finally {
lock.unlock(); // return 해도 finally 실행
}
}
}
lock() / unlock() 사용은?
답:
1. 패턴:
lock():
unlock():
주의:
try-finally 필수 이유:
임계 영역 중간에:
- 예외 발생
- return
- break/continue
→ unlock 안 되면 락 잔류
→ 다른 스레드 무한 대기 (데드락)
finally:
- 어떤 경우에도 실행
- unlock 보장
// ❌ try-finally 없음
lock.lock();
process(); // 예외 발생!
lock.unlock(); // ★ 실행 안 됨 (예외로 건너뜀)
// 락 영원히 잔류
// ✓ try-finally
lock.lock();
try {
process(); // 예외 발생
} finally {
lock.unlock(); // 예외 시에도 실행
}
// ❌ return 으로 unlock 누락 가능
public boolean check() {
lock.lock();
if (condition) {
return true; // ★ unlock 안 됨
}
lock.unlock(); // condition false 일 때만
return false;
}
// ✓ try-finally
public boolean checkSafe() {
lock.lock();
try {
if (condition) {
return true; // finally 실행됨
}
return false;
} finally {
lock.unlock(); // 모든 return 에서
}
}
// ✓ lock() 은 try 밖
lock.lock(); // try 밖
try {
// ...
} finally {
lock.unlock();
}
// 이유:
// - lock() 이 예외 던지면 (드물게)
// - try 안이면 unlock 호출 (락 없는데)
// - IllegalMonitorStateException
// lock() 은 try 직전, unlock 은 finally
try-finally 보장:
정상:
lock() → [임계 영역] → finally unlock()
예외:
lock() → [임계 영역 예외] → finally unlock()
↑ 보장
return:
lock() → [return] → finally unlock()
↑ 보장
@Service
public class TryFinallyExample {
private final ReentrantLock lock = new ReentrantLock();
private int stock = 100;
// ✓ try-finally (예외/return 안전)
public boolean reserve(int quantity) {
lock.lock();
try {
if (stock < quantity) {
return false; // return — finally 실행
}
stock -= quantity;
validateStock(); // 예외 가능 — finally 실행
return true;
} finally {
lock.unlock(); // 모든 경우 반납
}
}
private void validateStock() {
if (stock < 0) {
throw new IllegalStateException("Negative stock");
// 예외! 하지만 락 반납됨
}
}
}
try-finally가 필수인 이유는?
답:
1. 필수:
예외:
return:
위치:
unlock 누락 사고:
unlock 호출 안 됨:
- 락 영원히 보유
- 다른 스레드 무한 대기
- 데드락
원인:
- try-finally 없음
- finally 에 unlock 빠짐
- 예외/return 경로
// ❌ unlock 누락
public void process() {
lock.lock();
doWork(); // 예외!
lock.unlock(); // 실행 안 됨
}
// 결과:
// - 이 스레드: 예외로 빠짐 (락 보유한 채)
// - 다른 스레드: lock() 무한 대기
// - 데드락
# jstack 으로 진단
$ jstack <pid>
# 락 대기 스레드들:
"worker-1" WAITING
at jdk.internal.misc.Unsafe.park
- parking to wait for <0x...> (ReentrantLock)
"worker-2" WAITING
(같은 락 대기)
...
# 락 보유 스레드 (찾기 어려움)
# - 이미 다른 작업 중일 수도
# - 또는 예외로 사라짐
// ✓ 항상 try-finally
lock.lock();
try {
// 임계 영역
} finally {
lock.unlock();
}
// ✓ IDE/린트 도구 활용
// - SonarQube, SpotBugs
// - unlock 누락 경고
// ✓ 코드 리뷰
// - lock 다음 try-finally 확인
unlock 누락 위험:
ReentrantLock:
- 수동 unlock
- 깜박하면 데드락
- try-finally 필수
synchronized:
- 자동 반환
- 누락 불가능
- 안전
→ synchronized 가 이 점에서 안전
→ ReentrantLock 은 주의 필요
@Service
public class UnlockMissingProblem {
private final ReentrantLock lock = new ReentrantLock();
// ❌ 사고 — unlock 누락
public void dangerous(Shipment shipment) {
lock.lock();
process(shipment); // 예외 시 unlock 안 됨
lock.unlock();
// 예외 발생 시 락 잔류 → 데드락
}
// ✓ 안전 — try-finally
public void safe(Shipment shipment) {
lock.lock();
try {
process(shipment);
} finally {
lock.unlock(); // 항상 반납
}
}
private void process(Shipment s) {
if (s == null) throw new IllegalArgumentException();
// 예외 가능
}
}
unlock 누락의 사고는?
답:
1. 락 잔류:
데드락:
원인:
예방:
| 항목 | synchronized | ReentrantLock |
|---|---|---|
| 반환 | 자동 | 수동 (unlock) |
| 타임아웃 | X | tryLock |
| 인터럽트 | X | lockInterruptibly |
| 공정성 | X | 옵션 |
| Condition | wait/notify | 여러 Condition |
| 안전성 | 높음 (자동) | try-finally 필수 |
lock() vs synchronized 한 문장:
synchronized 는 자동 반환되는 간단한 동기화이고,
ReentrantLock 은 명시적 제어가 가능한 유연한 동기화이다.
synchronized: 간단, 자동, 제한적
ReentrantLock: 강력, 수동, 유연
ReentrantLock 장점:
1. 타임아웃 (tryLock)
2. 인터럽트 (lockInterruptibly)
3. 공정성 (fair)
4. Condition (정교한 대기)
5. tryLock (논블로킹 시도)
6. 락 상태 조회 (isLocked 등)
synchronized 장점:
1. 자동 반환 (안전)
2. 간단 (키워드)
3. 가독성
4. JVM 최적화
→ 단순한 경우 충분
선택:
synchronized:
- 단순한 동기화
- 자동 반환 원함
ReentrantLock:
- 타임아웃/인터럽트
- 공정성
- Condition
- 정교한 제어
권장:
- 먼저 synchronized
- 필요 시 ReentrantLock
@Service
public class LockVsSyncChoice {
private int counter = 0;
// synchronized — 단순
public synchronized void simpleSync() {
counter++;
}
// ReentrantLock — 정교
private final ReentrantLock lock = new ReentrantLock();
public boolean withTimeout() throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) { // 타임아웃
try {
counter++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
public void interruptible() throws InterruptedException {
lock.lockInterruptibly(); // 인터럽트
try {
counter++;
} finally {
lock.unlock();
}
}
}
lock() vs synchronized 차이는?
답:
1. 한 문장:
ReentrantLock 장점:
synchronized 장점:
선택:
재진입 (Reentrant):
같은 스레드가 이미 가진 락을
다시 획득 가능.
"Reentrant" Lock = 재진입 락
효과:
- 중첩 lock 가능
- 자기 데드락 방지
ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock(); // 카운트 1
try {
inner(); // 중첩
} finally {
lock.unlock(); // 카운트 0
}
}
public void inner() {
lock.lock(); // 카운트 2 (재진입)
try {
// ...
} finally {
lock.unlock(); // 카운트 1
}
}
재진입 카운트:
락 획득마다 카운트++
반납마다 카운트--
카운트 0 시 완전 반납
예:
lock() → 1
lock() → 2 (재진입)
unlock() → 1
unlock() → 0 (완전 반납)
주의:
- lock 횟수 = unlock 횟수
// 재진입 시 lock/unlock 짝 맞춰야
public void method() {
lock.lock(); // 1
try {
lock.lock(); // 2
try {
// ...
} finally {
lock.unlock(); // 1
}
} finally {
lock.unlock(); // 0
}
// lock 2번 = unlock 2번
}
재진입 없는 락:
같은 스레드가 재획득 시도:
- 자기 자신 대기
- 데드락 (self-deadlock)
ReentrantLock:
- 재진입 가능
- 같은 스레드 OK
- 안전
@Service
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void processAll(List<Shipment> shipments) {
lock.lock(); // 카운트 1
try {
for (Shipment s : shipments) {
processOne(s); // 중첩 lock
}
} finally {
lock.unlock(); // 카운트 0
}
}
public void processOne(Shipment shipment) {
lock.lock(); // 재진입 (카운트 2 if from processAll)
try {
doProcess(shipment);
} finally {
lock.unlock(); // 카운트 1
}
}
// 재진입 가능 → 중첩 호출 안전
// 락 상태 조회
public void checkLockState() {
log.info("Held by current: {}", lock.isHeldByCurrentThread());
log.info("Hold count: {}", lock.getHoldCount()); // 재진입 카운트
}
private void doProcess(Shipment s) { }
}
재진입성의 의미는?
답:
1. 재진입:
카운트:
짝 맞춤:
효과:
// 비공정 (기본)
ReentrantLock lock = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock(false);
// 공정
ReentrantLock fairLock = new ReentrantLock(true);
공정 vs 비공정:
공정 (Fair):
- FIFO 순서
- 먼저 대기 먼저 획득
- 기아 방지
- 성능 ↓
비공정 (Non-fair, 기본):
- 순서 보장 X
- 새치기 가능
- 처리량 ↑
- 기아 가능
비공정이 기본인 이유:
성능:
- 공정성 관리 비용 없음
- 처리량 ↑
- 컨텍스트 스위칭 ↓
대부분:
- 순서 중요하지 않음
- 처리량 우선
→ 기본 비공정 (성능)
공정 모드 비용:
- 대기 순서 관리 (큐)
- 새치기 방지
- 처리량 ↓ (순서 강제)
언제 공정:
- 순서 중요
- 기아 방지 필수
- 처리량보다 공평성
@Service
public class FairnessOption {
// 비공정 (기본, 성능)
private final ReentrantLock nonFairLock = new ReentrantLock();
public void processNonFair(Shipment shipment) {
nonFairLock.lock(); // 순서 보장 X (빠름)
try {
doProcess(shipment);
} finally {
nonFairLock.unlock();
}
}
// 공정 (순서 중요 시)
private final ReentrantLock fairLock = new ReentrantLock(true);
public void processFair(Shipment shipment) {
fairLock.lock(); // FIFO (먼저 온 순서)
try {
doProcess(shipment);
// 모든 shipment 공평하게 (기아 방지)
} finally {
fairLock.unlock();
}
}
private void doProcess(Shipment s) { }
}
공정성 옵션은?
답:
1. 공정 모드:
공정 vs 비공정:
기본:
공정 사용:
Condition:
ReentrantLock 의 대기/통지 메커니즘.
wait/notify 의 발전된 형태.
생성:
Condition cond = lock.newCondition();
메서드:
- await(): 대기 (wait 유사)
- signal(): 하나 통지 (notify 유사)
- signalAll(): 모두 통지 (notifyAll 유사)
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
public Object take() throws InterruptedException {
lock.lock();
try {
while (isEmpty()) {
notEmpty.await(); // 대기 (락 반납)
}
return remove();
} finally {
lock.unlock();
}
}
public void put(Object item) {
lock.lock();
try {
add(item);
notEmpty.signal(); // 통지
} finally {
lock.unlock();
}
}
// wait/notify 는 조건 1개
// Condition 은 여러 개 가능
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 가득 안 참
Condition notEmpty = lock.newCondition(); // 비지 않음
// 생산자: notFull 대기, notEmpty 통지
// 소비자: notEmpty 대기, notFull 통지
// → 정확한 조건별 대기/통지
// → 불필요한 깨어남 ↓
Condition vs wait/notify:
wait/notify:
- synchronized 와
- 조건 1개
- notifyAll 로 모두 깨움 (비효율)
Condition:
- ReentrantLock 과
- 여러 조건
- 조건별 정확한 통지
→ Condition 이 더 정교
// 생산자-소비자 (Condition, Phase 6 미리보기)
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int count, putIdx, takeIdx;
public BoundedBuffer(int size) {
items = new Object[size];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 가득 차면 대기
}
items[putIdx] = item;
putIdx = (putIdx + 1) % items.length;
count++;
notEmpty.signal(); // 소비자 통지
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 비면 대기
}
T item = (T) items[takeIdx];
takeIdx = (takeIdx + 1) % items.length;
count--;
notFull.signal(); // 생산자 통지
return item;
} finally {
lock.unlock();
}
}
}
@Service
public class ShipmentQueueWithCondition {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Queue<Shipment> queue = new LinkedList<>();
// 소비자
public Shipment consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 비면 대기
}
return queue.poll();
} finally {
lock.unlock();
}
}
// 생산자
public void produce(Shipment shipment) {
lock.lock();
try {
queue.offer(shipment);
notEmpty.signal(); // 소비자 깨움
} finally {
lock.unlock();
}
}
// 실무: BlockingQueue 권장 (이미 구현됨)
}
Condition으로 정교한 대기/통지는?
답:
1. Condition:
메서드:
여러 Condition:
장점:
| Q | 핵심 답변 |
|---|---|
| ReentrantLock? | Lock 구현체, 정교한 제어 |
| lock/unlock? | 명시적 획득/반납 |
| try-finally 필수? | 예외/return 시 반납 |
| unlock 누락? | 데드락 |
| lock vs sync? | 수동/유연 vs 자동/간단 |
| 재진입성? | 같은 스레드 재획득 |
| 공정성? | new ReentrantLock(true) |
| Condition? | 정교한 대기/통지 |
| Condition vs wait? | 여러 조건 |
| 락 상태 조회? | isLocked, getHoldCount |
답:
답:
답:
답:
답:
1. ReentrantLock
2. try-finally 필수
3. 추가 기능
이번 Unit에서 ReentrantLock 을 봤다면, 다음은 tryLock 데드락 회피 (★ 마스터).
🚀 Phase 5 — 정교한 락: LockSupport와 ReentrantLock
✅ Unit 5.1 synchronized의 한계 정리
✅ Unit 5.2 LockSupport
✅ Unit 5.3 ReentrantLock ← 여기
⏭ Unit 5.4 tryLock (★ 마스터) — Phase 5 완주
✅ Phase 1~4 (17 Unit, 1차 정점 완료)
🚀 Phase 5 — Lock 도구 (3/4 진행)
총: 20/35 Unit