LockSupport의 park·unpark
한 비트 허가증의 선행 신호·비누적 특성을 재현하고 조건 반복문과 `blocker`를 갖춘 직접 대기를 구현합니다.
LockSupport는 스레드를 WAITING 상태로 보내는 park와 다시 실행 가능하게 만드는 unpark를 제공합니다.
모니터 락을 획득하지 않고도 낮은 수준 대기점을 만들 수 있지만 신호 개수를 세는 큐가 아닙니다.
각 스레드에 최대 하나의 허가증이 있다는 모델로 읽어야 합니다.
unpark 허가증의 비누적성
아래 main은 작업자가 첫 park에 들어가기 전에 unpark(worker)를 두 번 호출합니다.
허가증은 누적되지 않아 첫 park만 즉시 반환하고 두 번째 park에서 멈춥니다.
실제로 wrong-second-permit=false가 출력됩니다.
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();
}
}
}두 신호를 보존해야 한다면 Semaphore 허가증, BlockingQueue 항목, CountDownLatch count처럼 수량을 표현하는 추상화를 사용합니다.
LockSupport 허가증을 이벤트 카운터로 쓰면 신호가 합쳐져 작업이 멈춥니다.
선행 unpark와 허가증
wait와 notify를 조건 없이 잘못 사용하면 notify가 먼저 발생한 뒤 대기자가 영원히 잘 수 있습니다.
LockSupport는 대상 스레드에 허가증을 기록하므로 선행 unpark 뒤 park가 즉시 반환할 수 있습니다.
대상 Thread 참조를 알아야 하며 호출 대상이 아직 start되지 않았어도 허가증 개념은 적용됩니다.
허가증이 없으면 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();
}
}
}unpark가 먼저인데도 작업자는 멈추지 않고 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();
}
}volatile open = true가 조건 공개를 맡고 unpark가 대기를 깨웁니다.
이 단일 대기자 예제는 교육용입니다.
여러 대기자, 시간 제한, 취소, 큐 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");
}
}큐 조건은 jobs 모니터 아래 검사·변경하고 park는 빈 상태에서 CPU를 놓습니다.
submit과 close가 unpark해 조건을 다시 검사하게 합니다.
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();
}
}
}조건을 먼저 publish하므로 unpark와 park 사이의 순서가 바뀌어도 결과가 유지됩니다.
여러 번의 업무 이벤트를 표현하려면 큐 또는 세마포어로 바꿉니다.
LockSupport의 핵심은 허가증 하나가 선행 unpark를 기억하되 여러 신호를 누적하지 않는다는 점입니다.
park 반환 뒤 조건을 재검사하고 blocker로 원인을 남기며, 가능한 경우 더 높은 수준의 동기화 도구를 선택해야 합니다.