LockSupport의 park·unpark
한 비트 허가증의 선행 신호·비누적 특성을 재현하고 조건 반복문과 blocker 정보를 갖춘 직접 대기를 구현합니다.
LockSupport는 허가증이 없을 때 대기할 수 있는 park와 대상 스레드에 허가증을 제공하는 unpark를 제공합니다.
모니터 락을 획득하지 않고도 낮은 수준 대기점을 만들 수 있지만 신호 개수를 세는 큐가 아닙니다.
각 스레드에 최대 하나의 허가증이 있다는 모델로 읽어야 합니다.
unpark 허가증의 비누적성
아래 main은 작업자가 첫 park에 들어가기 전에 unpark(worker)를 두 번 호출합니다.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;
public final class DoubleUnparkPermitBug {
public static void main(String[] args) throws InterruptedException {
CountDownLatch mayPark = new CountDownLatch(1), firstReturned = new CountDownLatch(1);
Thread worker =
new Thread(
() -> {
await(mayPark);
LockSupport.park();
firstReturned.countDown();
LockSupport.park();
},
"double-park");
worker.start();
LockSupport.unpark(worker);
LockSupport.unpark(worker);
mayPark.countDown();
firstReturned.await();
worker.join(20);
System.out.println("wrong-second-permit=" + !worker.isAlive());
worker.interrupt();
worker.join();
}
private static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}DoubleUnparkPermitBug의 래치 대기, 첫 park 뒤 신호, 두 번째 park와 생존 조회를 실제 순서로 읽고 관측 한계를 구분합니다.
| 원문의 지점 | 가능한 진행 | 판단의 한계 |
|---|---|---|
mayPark 래치 대기 | 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다. | 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다. |
첫 park 뒤 firstReturned 신호 | 첫 park가 반환해야 신호를 보냅니다. | 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다. |
| 두 번째 park와 생존 조회 | 두 번째 park도 임의로 반환할 수 있습니다. | 출력에 도달해도 wrong-second-permit은 false 또는 true일 수 있습니다. |
mayPark래치 대기- 가능한 진행: 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다.판단의 한계: 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다.
- 첫 park 뒤
firstReturned신호 - 가능한 진행: 첫 park가 반환해야 신호를 보냅니다.판단의 한계: 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다.
- 두 번째 park와 생존 조회
- 가능한 진행: 두 번째 park도 임의로 반환할 수 있습니다.판단의 한계: 출력에 도달해도
wrong-second-permit은 false 또는 true일 수 있습니다.
false는 조회 시 작업자가 살아 있다는 뜻이며 두 번째 park에 이미 들어갔다는 증거가 아닙니다. 정리용 interrupt는 출력 뒤에 있으므로 앞선 래치 대기에서 멈춘 경로를 구제하지 못합니다.
두 신호를 보존해야 한다면 Semaphore 허가증, BlockingQueue 항목, CountDownLatch count처럼 수량을 표현하는 추상화를 사용합니다.
LockSupport 허가증을 이벤트 카운터로 쓰면 신호가 합쳐져 작업이 멈춥니다.
선행 unpark와 허가증
wait와 notify를 조건 없이 잘못 사용하면 notify가 먼저 발생한 뒤 대기자가 영원히 잘 수 있습니다.
LockSupport는 대상 스레드에 허가증을 기록하므로 선행 unpark 뒤 park가 즉시 반환할 수 있습니다.
대상 Thread 참조를 알아야 하며 아직 시작하지 않은 스레드에 대한 unpark는 효과를 보장하지 않습니다.
허가증이 없으면 park는 WAITING으로 들어갈 수 있습니다.
허가증이 있으면 소비하고 즉시 돌아옵니다.
park는 임의로 반환할 수 있고 interrupt 상태가 true여도 반환하므로 “돌아왔다”를 조건 충족과 동일시하면 안 됩니다.
항상 별도 조건을 반복문에서 재검사합니다.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;
public final class OneBitPermitOrder {
public static void main(String[] args) throws InterruptedException {
CountDownLatch enter = new CountDownLatch(1);
Thread worker =
new Thread(
() -> {
await(enter);
LockSupport.park("first-permit");
System.out.println("park-returned");
},
"permit-worker");
worker.start();
LockSupport.unpark(worker);
enter.countDown();
worker.join();
System.out.println("state=" + worker.getState());
}
private static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}이 원문도 래치 내부 대기가 허가증을 소비하면 명시적 park와 join에서 계속 기다릴 수 있습니다. 정상 완료한 경우에는 park-returned 다음 state=TERMINATED가 출력됩니다.
허가증과 업무 조건
허가증은 JVM 내부 대기 제어이고 업무가 준비됐다는 사실 자체를 저장하지 않습니다.
조건을 volatile 또는 락으로 공개하고 park는 CPU를 놓는 수단으로만 사용합니다.
임의 반환, 무관한 unpark, interrupt 뒤에도 조건이 false면 정책에 따라 다시 기다립니다.
import java.util.concurrent.locks.LockSupport;
public final class ParkedConditionGate {
private volatile boolean open;
private volatile Thread waiter;
public void awaitOpen() {
waiter = Thread.currentThread();
while (!open) {
LockSupport.park(this);
if (Thread.interrupted()) throw new IllegalStateException("gate wait interrupted");
}
}
public void open() {
open = true;
Thread target = waiter;
if (target != null) LockSupport.unpark(target);
}
public static void main(String[] args) throws InterruptedException {
ParkedConditionGate gate = new ParkedConditionGate();
Thread worker =
new Thread(
() -> {
gate.awaitOpen();
System.out.println("gate-open");
},
"condition-waiter");
worker.start();
while (worker.getState() != Thread.State.WAITING) {
Thread.onSpinWait();
}
gate.open();
worker.join();
}
}ParkedConditionGate의 open 쓰기, 조건 반복, 인터럽트 처리와 성공 반환을 실제 코드로 비교합니다.
| 원문의 경로 | 조건·신호 처리 | 이어지는 동작 |
|---|---|---|
open() 호출 | volatile open을 true로 쓴 뒤 등록된 대기자를 unpark합니다. | 조건 공개가 대기 해제 요청보다 먼저입니다. |
while (!open) 안 | park 뒤 Thread.interrupted()를 검사합니다. | true이면 플래그를 지우고 예외를 던집니다. |
open이 true인 검사 | 반복문을 건너뛰거나 빠져나옵니다. | 호출자가 gate-open을 출력합니다. |
open()호출- 조건·신호 처리: volatile
open을 true로 쓴 뒤 등록된 대기자를 unpark합니다.이어지는 동작: 조건 공개가 대기 해제 요청보다 먼저입니다. while (!open)안- 조건·신호 처리: park 뒤
Thread.interrupted()를 검사합니다.이어지는 동작: true이면 플래그를 지우고 예외를 던집니다. open이 true인 검사- 조건·신호 처리: 반복문을 건너뛰거나 빠져나옵니다.이어지는 동작: 호출자가
gate-open을 출력합니다.
원문의 main은 interrupt를 보내지 않습니다. 또 WAITING 관측을 기다리는 반복문에 시간 제한이 없으므로 이 예제를 종료 시간이 정해진 조정 도구로 볼 수 없습니다.
이 단일 대기자 예제는 교육용입니다.
여러 대기자, 시간 제한, 취소, 큐 removal을 직접 구현하면 검증 부담이 급격히 커지므로 운영에서는 동기화 도구와 동시성 컬렉션을 우선합니다.
blocker와 대기 원인
LockSupport.park(blocker)에 넘긴 객체는 진단 도구에서 해당 스레드의 대기 원인 객체로 표시될 수 있습니다.
업무상 비밀 정보가 아닌 안정적인 소유자 객체를 전달합니다.
LockSupport.getBlocker(thread)로 관찰할 수도 있지만 동기화 제어에 이 조회를 사용하지 않습니다.
blocker는 모니터 락이 아닙니다.
park가 blocker 객체를 잠그지 않으므로 다른 스레드가 synchronized(blocker)에 들어가는 것과 직접 관계가 없습니다.
스레드 덤프에서 parking to wait for와 waiting to lock을 구별합니다.
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;
public final class ParkedJobDispatcher {
private final Queue<String> jobs = new ArrayDeque<>();
private final Thread worker = new Thread(this::consume, "park-dispatcher");
private boolean closed;
public void start() {
worker.start();
}
public void submit(String job) {
synchronized (jobs) {
if (closed) throw new IllegalStateException("closed");
jobs.add(job);
}
LockSupport.unpark(worker);
}
public void close() throws InterruptedException {
synchronized (jobs) {
closed = true;
}
LockSupport.unpark(worker);
worker.join();
}
private void consume() {
for (; ; ) {
String job;
synchronized (jobs) {
job = jobs.poll();
if (job == null && closed) return;
}
if (job == null) {
LockSupport.park(this);
continue;
}
System.out.println("handled=" + job);
}
}
public static void main(String[] args) throws InterruptedException {
ParkedJobDispatcher d = new ParkedJobDispatcher();
d.start();
d.submit("index");
d.submit("publish");
d.close();
System.out.println("dispatcher-closed");
}
}ParkedJobDispatcher의 submit, consume, close가 jobs 모니터 안에서 다루는 상태와 모니터 밖에서 하는 호출을 비교합니다.
| 원문의 경로 | jobs 모니터 안 | 모니터 밖 |
|---|---|---|
submit | 종료 여부를 확인하고 작업을 큐에 넣습니다. | 작업자를 unpark합니다. |
consume | 작업을 꺼내고, 비었으며 닫혔으면 반환합니다. | 비었으면 park(this) 뒤 재검사하고, 작업이 있으면 로그를 출력합니다. |
close | closed를 true로 바꿉니다. | unpark 뒤 join하여 작업자 종료를 기다립니다. |
submit- jobs 모니터 안: 종료 여부를 확인하고 작업을 큐에 넣습니다.모니터 밖: 작업자를 unpark합니다.
consume- jobs 모니터 안: 작업을 꺼내고, 비었으며 닫혔으면 반환합니다.모니터 밖: 비었으면
park(this)뒤 재검사하고, 작업이 있으면 로그를 출력합니다. close- jobs 모니터 안:
closed를 true로 바꿉니다.모니터 밖: unpark 뒤 join하여 작업자 종료를 기다립니다.
상태를 보호하는 모니터는 jobs이고 park의 진단 객체는 this입니다. 외부 interrupt를 처리하지 않으므로 빈 큐에서 반복 실행할 수 있지만 원문의 main은 이 경로를 실행하지 않습니다.
BlockingQueue가 이미 이 요구를 더 완전하게 제공하므로 직접 구현은 원리 이해 용도입니다.
park를 직접 쓸지 판단하는 기준
락, 세마포어, 래치, 블로킹 큐로 요구를 표현할 수 있으면 그 API를 사용합니다.
LockSupport는 동기화 도구 구현, 실행 시점 스케줄러, 특수 low-수준 라이브러리처럼 조건 큐와 취소를 직접 소유해야 할 때 검토합니다.
park 반환 원인은 허가증, interrupt, 시간 제한, 임의 반환 중 하나일 수 있습니다.
원인을 단일 boolean으로 추측하지 말고 업무 조건과 interrupt 상태를 다시 확인합니다.
신호 수량을 잃으면 안 되는 작업 흐름에는 한 비트짜리 허가증만 사용하지 않습니다.
연습 문제
main이 ready=true를 기록하고 작업자를 unpark한 다음에 작업자가 park 지점에 도착해도 정상 완료되는 예제를 작성하세요.
조건은 volatile이어야 하고 작업자는 while 반복문에서 검사해야 합니다.
정답과 선행 신호 처리
작업자 진입을 래치로 늦춘 사이 main이 조건과 허가증을 먼저 설정합니다.
작업자는 조건이 이미 true라 park를 호출하지 않아도 됩니다.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;
public final class EarlyUnparkConditionSolution {
private static volatile boolean ready;
public static void main(String[] args) throws InterruptedException {
CountDownLatch enter = new CountDownLatch(1);
Thread worker =
new Thread(
() -> {
await(enter);
while (!ready) {
LockSupport.park();
}
System.out.println("observed-ready");
},
"early-signal-worker");
worker.start();
ready = true;
LockSupport.unpark(worker);
enter.countDown();
worker.join();
}
private static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}래치도 값 공개를 보장하므로 이 실행만으로 volatile의 효과나 명시적 park의 허가증 소비를 따로 입증하지는 않습니다.
여러 번의 업무 이벤트를 표현하려면 큐 또는 세마포어로 바꿉니다.
LockSupport의 핵심은 허가증 하나가 선행 unpark를 기억하되 여러 신호를 누적하지 않는다는 점입니다.
park 반환 뒤 조건을 재검사하고 blocker로 원인을 남기며, 가능한 경우 더 높은 수준의 동기화 도구를 선택해야 합니다.