F-LAB JAVA · 4주차 · Phase 5 · 정교한 락: LockSupport와 ReentrantLock
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
LockSupport 는 스레드를 멈추고 (park) 깨우는 (unpark) 저수준 도구로, ReentrantLock 등 고수준 동기화 도구의 내부 구현에 사용된다.
park()는 현재 스레드를 WAITING 상태로 멈추고,parkNanos(nanos)는 TIMED_WAITING 으로 시간 한정 멈춤,unpark(thread)는 대상 스레드를 RUNNABLE 로 복귀시킨다.
park 된 스레드가 BLOCKED 가 아닌 WAITING 인 이유는 락 경쟁이 아니라 직접적인 일시정지 이기 때문이며, 인터럽트로도 깨울 수 있다 (synchronized 와 달리).
LockSupport 는 permit (허가) 기반 으로 동작하여, unpark 가 park 보다 먼저 호출되어도 permit 가 저장되어 다음 park 가 즉시 통과한다 (순서 문제 완화).
너무 저수준이라 실무에서 직접 쓰지 않으며, ReentrantLock·CompletableFuture·Executor 등 고수준 도구의 기반으로 동작한다.
LockSupport = 손님 호출 벨 (직접):
park() = 손님이 잠듦:
- 의자에서 대기 (WAITING)
- 누가 깨울 때까지
unpark(손님) = 벨로 깨움:
- 특정 손님 깨움
- RUNNABLE 복귀
permit (허가) = 미리 누른 벨:
- 손님 자기 전에 벨 누름 (unpark 먼저)
- 손님이 자려고 하면 (park)
- 이미 벨 울렸으니 안 잠 (즉시 통과)
- 순서 무관
인터럽트로 깨움:
- 흔들어서도 깨울 수 있음 (synchronized 와 다름)
너무 저수준:
- 직접 벨 시스템 만들기 복잡
- ReentrantLock 이 잘 포장
→ LockSupport = park/unpark 저수준 도구, permit 기반, 고수준 도구의 기반.
1. LockSupport의 정의
2. park()의 동작
3. unpark()의 동작
4. park 상태가 WAITING인 이유
5. permit (허가) 메커니즘
6. 인터럽트와 park
7. wait/notify vs park/unpark
8. 직접 안 쓰는 이유 (고수준 기반)
9. 면접 + 자기 점검
LockSupport:
java.util.concurrent.locks.LockSupport
스레드를 멈추고 (park) 깨우는 (unpark)
저수준 도구.
특징:
- 스레드 단위 제어
- permit 기반
- 고수준 도구의 기반
public class LockSupport {
// 멈춤
static void park(); // WAITING
static void park(Object blocker); // WAITING (blocker 정보)
static void parkNanos(long nanos); // TIMED_WAITING
static void parkUntil(long deadline); // TIMED_WAITING (절대 시각)
// 깨움
static void unpark(Thread thread); // 대상 스레드 깨움
}
Thread t = new Thread(() -> {
System.out.println("Before park");
LockSupport.park(); // 멈춤 (WAITING)
System.out.println("After park (unparked)");
});
t.start();
Thread.sleep(1000);
LockSupport.unpark(t); // 깨움
// t 가 "After park" 출력
LockSupport vs Thread.sleep:
Thread.sleep(ms):
- 시간만큼 대기 (TIMED_WAITING)
- 외부에서 못 깨움 (인터럽트 외)
LockSupport.park():
- 무한 대기 (WAITING)
- unpark 로 깨움
- 인터럽트로도 깸
park 가 더 유연 (제어 가능)
public class LockSupportBasics {
private Thread worker;
public void startWorker() {
worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
if (hasWork()) {
processWork();
} else {
LockSupport.park(); // 일 없으면 멈춤
}
}
});
worker.start();
}
public void notifyWork() {
LockSupport.unpark(worker); // 일 생기면 깨움
}
private boolean hasWork() { return false; }
private void processWork() { }
// 실무: 직접 쓰기보다 BlockingQueue, Lock 권장
}
LockSupport의 정의는?
답:
1. 정의:
메서드:
특징:
역할:
park():
현재 스레드를 WAITING 상태로 멈춤.
깨어나는 조건:
- unpark(현재 스레드)
- 인터럽트
- (spurious wakeup 가능)
// parkNanos — 시간 한정
LockSupport.parkNanos(1_000_000_000L); // 1초 (나노초)
// TIMED_WAITING
// 1초 또는 unpark 또는 인터럽트
// parkUntil — 절대 시각
LockSupport.parkUntil(System.currentTimeMillis() + 1000);
park 흐름:
스레드:
실행 중 (RUNNABLE)
↓ LockSupport.park()
WAITING (멈춤)
↓ unpark / 인터럽트
RUNNABLE (재개)
// spurious wakeup (가짜 깨어남) 대비
public void parkWithCheck() {
while (!condition) { // while 로 재확인
LockSupport.park();
// 깨어나도 조건 재확인
// park 는 이유 없이 깨어날 수 있음
}
}
// park 후 조건 확인 필수
// park(Object blocker) — 진단 정보
LockSupport.park(this); // blocker 객체 지정
// 효과:
// - jstack 등에서 무엇을 기다리는지 표시
// - 디버깅 도움
// 일반 park 는 blocker 없음 (정보 적음)
public class ParkExample {
private volatile boolean ready = false;
public void waitForReady() {
while (!ready) { // 조건 확인
LockSupport.parkNanos(this, 100_000_000L); // 0.1초 (blocker)
// ready 될 때까지 또는 0.1초마다 확인
}
log.info("Ready!");
}
public void setReady() {
ready = true;
// park 한 스레드는 다음 확인 시 통과
// 또는 unpark 로 즉시
}
}
park()의 동작은?
답:
1. park():
parkNanos:
spurious wakeup:
blocker:
unpark(Thread thread):
대상 스레드를 깨움 (RUNNABLE).
특징:
- 대상 스레드 지정
- permit 부여
- park 보다 먼저 호출 가능 (permit 저장)
Thread worker = new Thread(() -> {
LockSupport.park(); // 멈춤
process();
});
worker.start();
// 깨움
LockSupport.unpark(worker); // worker 깨움
unpark 의 특징 — 대상 지정:
unpark(특정 스레드)
- 그 스레드만 깨움
대조 (notify):
- notify() 는 wait 중인 임의 스레드
- 대상 지정 X
park/unpark 는 정확한 제어
park / unpark:
스레드 A (worker):
park() → WAITING
스레드 B (main):
unpark(A) → A 를 RUNNABLE 로
스레드 A:
WAITING → RUNNABLE (재개)
unpark 의 permit 누적:
permit 은 최대 1개 (이진).
unpark 여러 번:
- permit 1개만 (누적 X)
- park 한 번만 통과
예:
unpark(t); // permit 1
unpark(t); // 여전히 permit 1 (누적 X)
// park 한 번만 통과
public class UnparkExample {
private final Map<Long, Thread> waitingWorkers = new ConcurrentHashMap<>();
public void workerWait(Long shipmentId) {
waitingWorkers.put(shipmentId, Thread.currentThread());
LockSupport.park(); // 멈춤
waitingWorkers.remove(shipmentId);
}
public void notifyWorker(Long shipmentId) {
Thread worker = waitingWorkers.get(shipmentId);
if (worker != null) {
LockSupport.unpark(worker); // 특정 worker 깨움
}
}
// 특정 스레드 지정 깨움 (notify 와 차이)
}
unpark()의 동작은?
답:
1. unpark:
대상 지정:
permit:
누적 X:
질문:
park 된 스레드가 BLOCKED 가 아닌
WAITING 인 이유는?
답:
- 락 경쟁이 아니라 직접 일시정지
- 모니터 락 대기 (BLOCKED) X
- 명시적 대기 (WAITING)
BLOCKED:
- synchronized 락 대기
- 락 경쟁
- 락 반납 시 깸
WAITING:
- wait(), join(), park()
- 명시적/직접 대기
- 신호로 깸 (notify, unpark)
park 가 WAITING 인 이유:
park 는 락 경쟁이 아님.
- 스레드를 직접 멈춤
- 락 대기 X
- unpark 신호 대기
→ WAITING (락 아닌 신호 대기)
→ parkNanos 는 TIMED_WAITING
Thread t = new Thread(() -> {
LockSupport.park();
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // WAITING
// parkNanos:
Thread t2 = new Thread(() -> {
LockSupport.parkNanos(10_000_000_000L);
});
t2.start();
Thread.sleep(100);
System.out.println(t2.getState()); // TIMED_WAITING
상태 비교:
synchronized 대기:
스레드 → BLOCKED (락 경쟁)
park:
스레드 → WAITING (신호 대기)
parkNanos:
스레드 → TIMED_WAITING (시간 + 신호)
public class ParkStateExample {
public void demonstrateStates() throws InterruptedException {
// park → WAITING
Thread parkThread = new Thread(LockSupport::park);
parkThread.start();
Thread.sleep(100);
log.info("Park state: {}", parkThread.getState()); // WAITING
LockSupport.unpark(parkThread);
// parkNanos → TIMED_WAITING
Thread parkNanosThread = new Thread(() ->
LockSupport.parkNanos(5_000_000_000L));
parkNanosThread.start();
Thread.sleep(100);
log.info("ParkNanos state: {}", parkNanosThread.getState()); // TIMED_WAITING
LockSupport.unpark(parkNanosThread);
}
}
park 상태가 WAITING인 이유는?
답:
1. 이유:
vs BLOCKED:
상태:
깨움:
permit (허가):
LockSupport 의 핵심 메커니즘.
- 각 스레드가 permit 1개 (이진)
- park: permit 소비 (없으면 대기)
- unpark: permit 부여
permit 동작:
park():
- permit 있으면 → 즉시 통과 (permit 소비)
- permit 없으면 → 대기 (WAITING)
unpark():
- permit 부여 (최대 1개)
핵심:
- permit 으로 순서 문제 완화
unpark 가 park 보다 먼저:
일반적 우려:
- unpark 먼저 → 신호 사라짐?
- park 가 영원히 대기?
permit 으로 해결:
- unpark 가 permit 저장
- 나중 park 가 permit 보고 즉시 통과
→ 순서 무관 (어느 정도)
permit 순서 무관:
케이스 1 (park 먼저):
park() → 대기 (permit 없음)
unpark() → permit 부여 → 깸
케이스 2 (unpark 먼저):
unpark() → permit 부여 (저장)
park() → permit 있음 → 즉시 통과
→ 둘 다 동작 (순서 무관)
permit vs wait/notify 순서:
wait/notify:
- notify 먼저 → 신호 사라짐
- 나중 wait → 영원히 대기 (lost signal)
park/unpark (permit):
- unpark 먼저 → permit 저장
- 나중 park → 즉시 통과
- lost signal 완화
→ park/unpark 가 순서에 강함
public class PermitExample {
public void demonstratePermit() throws InterruptedException {
Thread worker = new Thread(() -> {
// unpark 가 먼저 와도 OK (permit)
Thread.sleep(200); // 일부러 늦게 park
LockSupport.park(); // permit 있으면 즉시 통과
log.info("Worker proceeded");
});
worker.start();
// park 보다 먼저 unpark
Thread.sleep(50);
LockSupport.unpark(worker); // permit 부여 (저장)
// worker 가 나중에 park 해도 permit 으로 통과
// wait/notify 였다면 lost signal (영원히 대기)
// park/unpark 는 permit 으로 안전
}
}
permit (허가) 메커니즘은?
답:
1. permit:
동작:
순서 완화:
vs wait/notify:
park 와 인터럽트:
park 된 스레드는 인터럽트로 깸.
- interrupt() 호출
- park 에서 깨어남 (RUNNABLE)
- 단, InterruptedException 던지지 X
- 인터럽트 플래그 확인 필요
synchronized BLOCKED:
- 인터럽트 X (못 깸)
- 락 받을 때까지
park WAITING:
- 인터럽트 O (깸)
- 더 유연
→ park 가 인터럽트 가능 (장점)
Thread worker = new Thread(() -> {
LockSupport.park();
// park 에서 깨어남 (unpark 또는 인터럽트)
if (Thread.currentThread().isInterrupted()) {
log.info("Interrupted");
return; // 인터럽트면 종료
}
// unpark 면 계속
process();
});
worker.start();
worker.interrupt(); // park 깨움 (가능!)
park 의 인터럽트 특이점:
park():
- 인터럽트로 깸
- 하지만 InterruptedException X
- 플래그만 설정
- 직접 확인 필요
대조 (wait, sleep):
- InterruptedException 던짐
인터럽트와 park:
park:
스레드 → WAITING
interrupt() → 깸 (RUNNABLE)
플래그 설정 (예외 X)
→ 직접 확인
synchronized:
스레드 → BLOCKED
interrupt() → 무시 (안 깸)
public class ParkInterruptExample {
public void interruptibleWorker() {
Thread worker = new Thread(() -> {
while (true) {
LockSupport.park(); // 인터럽트로 깰 수 있음
if (Thread.currentThread().isInterrupted()) {
log.info("Worker shutting down");
break; // 종료
}
processNextTask();
}
});
worker.start();
// 종료 시 인터럽트로 깸 (synchronized 와 달리 가능)
worker.interrupt();
}
private void processNextTask() { }
}
인터럽트로 park를 깨울 수 있는가?
답:
1. 가능:
vs synchronized:
특이점:
활용:
| 항목 | wait/notify | park/unpark |
|---|---|---|
| 락 필요 | O (synchronized) | X |
| 대상 지정 | X (notify) | O (unpark) |
| 순서 | lost signal | permit 완화 |
| 위치 | synchronized 안 | 어디서나 |
| 인터럽트 | InterruptedException | 플래그 |
wait/notify 제약:
- synchronized 안에서만
- 모니터 락 필요
- notify 는 임의 스레드
- lost signal (순서 문제)
park/unpark 자유:
- 락 불필요
- 어디서나 호출
- unpark 대상 지정
- permit (순서 완화)
// ❌ wait/notify — lost signal
synchronized (lock) {
lock.notify(); // wait 중 없으면 신호 사라짐
}
// 나중에:
synchronized (lock) {
lock.wait(); // 영원히 대기 (신호 놓침)
}
// ✓ park/unpark — permit
LockSupport.unpark(t); // permit 저장
// 나중에:
LockSupport.park(); // permit 으로 즉시 통과
선택:
wait/notify:
- 조건 동기화 (synchronized 와)
- 생산자-소비자 (Phase 6)
- 모니터 패턴
park/unpark:
- 저수준 제어
- 락 프레임워크 구현
- 직접 쓰기 드묾
실무:
- 둘 다 직접 쓰기보다
- 고수준 도구 (BlockingQueue, Lock)
public class WaitVsParkExample {
// wait/notify (synchronized 안)
private final Object lock = new Object();
private boolean ready = false;
public void waitWithMonitor() throws InterruptedException {
synchronized (lock) {
while (!ready) {
lock.wait(); // 락 필요
}
}
}
public void notifyWithMonitor() {
synchronized (lock) {
ready = true;
lock.notify();
}
}
// park/unpark (락 불필요)
private Thread waiter;
public void waitWithPark() {
waiter = Thread.currentThread();
while (!ready) {
LockSupport.park(); // 락 불필요
}
}
public void notifyWithPark() {
ready = true;
LockSupport.unpark(waiter); // 대상 지정
}
// 실무: BlockingQueue 등 권장
}
wait/notify vs park/unpark는?
답:
1. wait/notify:
park/unpark:
선택:
실무:
LockSupport 직접 안 쓰는 이유:
1. 너무 저수준
- permit 직접 관리
- 복잡
2. 실수 위험
- 조건 확인 누락
- spurious wakeup
3. 고수준 도구 존재
- ReentrantLock
- BlockingQueue
- CompletableFuture
ReentrantLock 내부 — LockSupport:
ReentrantLock 의 락 대기:
- AQS (AbstractQueuedSynchronizer)
- 내부적으로 LockSupport.park/unpark
AQS:
- 대기 큐 관리
- park 로 대기
- unpark 로 깨움
→ LockSupport 는 AQS 의 기반
AQS:
동기화 도구의 프레임워크.
- ReentrantLock
- Semaphore
- CountDownLatch
- 등의 기반
내부:
- state (상태)
- 대기 큐 (FIFO)
- LockSupport.park/unpark
→ LockSupport 가 AQS 의 park/unpark
// ❌ LockSupport 직접 (저수준)
private Thread waiter;
public void wait() {
waiter = Thread.currentThread();
LockSupport.park();
}
public void signal() {
LockSupport.unpark(waiter);
}
// ✓ 고수준 — BlockingQueue
private final BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
public Task take() throws InterruptedException {
return queue.take(); // 비면 자동 대기 (내부 park)
}
public void put(Task task) throws InterruptedException {
queue.put(task); // 자동 통지
}
// ✓ 고수준 — ReentrantLock + Condition
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void waitForCondition() throws InterruptedException {
lock.lock();
try {
condition.await(); // 내부 park
} finally {
lock.unlock();
}
}
그래도 LockSupport 를 알아야:
- 면접 (저수준 이해)
- 고수준 도구 내부 이해
- 디버깅 (jstack 의 park)
- 프레임워크 개발 (드물게)
→ 직접 쓰진 않지만 이해는 중요
@Service
public class HighLevelTools {
// ❌ LockSupport 직접 (안 씀)
// ✓ BlockingQueue (내부 LockSupport)
private final BlockingQueue<Shipment> queue = new LinkedBlockingQueue<>();
public void produce(Shipment shipment) throws InterruptedException {
queue.put(shipment); // 가득 차면 대기 (내부 park)
}
public Shipment consume() throws InterruptedException {
return queue.take(); // 비면 대기 (내부 park)
}
// ✓ ReentrantLock (내부 AQS + LockSupport)
private final ReentrantLock lock = new ReentrantLock();
public void process(Shipment shipment) {
lock.lock(); // 내부적으로 LockSupport.park 사용
try {
doProcess(shipment);
} finally {
lock.unlock();
}
}
// 고수준 도구가 LockSupport 잘 포장
private void doProcess(Shipment s) { }
}
LockSupport를 직접 안 쓰는 이유는?
답:
1. 너무 저수준:
고수준 기반:
AQS:
알아야 하는 이유:
| Q | 핵심 답변 |
|---|---|
| LockSupport? | park/unpark 저수준 |
| park()? | WAITING 멈춤 |
| parkNanos()? | TIMED_WAITING |
| unpark()? | 대상 스레드 깸 |
| park 상태? | WAITING |
| 왜 WAITING? | 락 아닌 신호 대기 |
| 인터럽트로 깸? | 가능 |
| permit? | 허가 (순서 완화) |
| wait/notify 차이? | 락 불필요, 대상 지정 |
| 직접 안 쓰는 이유? | 저수준, 고수준 기반 |
답:
답:
답:
답:
답:
1. LockSupport
2. 특징
3. 직접 안 씀
이번 Unit에서 LockSupport 를 봤다면, 다음은 ReentrantLock (실무 표준).
🚀 Phase 5 — 정교한 락: LockSupport와 ReentrantLock
✅ Unit 5.1 synchronized의 한계 정리
✅ Unit 5.2 LockSupport ← 여기
⏭ Unit 5.3 ReentrantLock
⏭ Unit 5.4 tryLock (★ 마스터)
✅ Phase 1~4 (17 Unit, 1차 정점 완료)
🚀 Phase 5 — Lock 도구 (2/4 진행)
총: 19/35 Unit