LockSupport의 park·unpark
한 비트 허가증의 선행 신호·비누적 특성을 재현하고 조건 반복문과 blocker 정보를 갖춘 직접 대기를 구현합니다.
LockSupport는 허가증이 없을 때 대기할 수 있는 park와 대상 스레드에 허가증을 제공하는 unpark를 제공합니다.
모니터 락을 획득하지 않고도 낮은 수준 대기점을 만들 수 있지만 신호 개수를 세는 큐가 아닙니다.
각 스레드에 최대 하나의 허가증이 있다는 모델로 읽어야 합니다.
허가증과 데이터 공개를 별개로 설계합니다. unpark 한 번을 업무 한 건의 저장이나 임의 필드의 안전한 공개로 간주하지 않습니다. 준비 사실은 volatile 조건 또는 같은 락으로 보호한 큐에 남기고, unpark는 그 사실을 다시 확인할 기회를 주는 데 사용합니다. 실제 항목 수가 중요하다면 한 비트 대신 큐·세마포어의 수량이 필요합니다. 허가증을 받은 뒤 또 다른 내부 대기를 거치면 그 대기가 같은 허가증을 소비할 수 있으므로, 조건 검사와 명시적 park 사이의 호출도 검토해야 합니다.
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();
}
}
}두 번의 unpark만으로 두 park의 결과를 고정할 수 없다
선행 unpark 두 번도 허가증 최대 1이며 내부 대기가 소비할 수 있는 두 가능한 경로입니다. 조건부 소비·대기·관측을 단계로 강조하되 고정 출력이나 두 허가증을 주장하지 않습니다.
DoubleUnparkPermitBug의 래치 대기, 첫 park 뒤 신호, 두 번째 park와 생존 조회를 실제 순서로 읽고 관측 한계를 구분합니다.
PARK · 허가증과 관측
두 번의 unpark만으로 두 park의 결과를 고정할 수 없다
unpark 허가증은 쌓이지 않습니다. 중간 래치 대기가 소비할 수 있어 첫 park 뒤 출력도 보장되지 않습니다.
mayPark래치 대기- 가능한 진행
- 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다.
- 판단의 한계
- 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다.
첫 park 뒤
firstReturned신호- 가능한 진행
- 첫 park가 반환해야 신호를 보냅니다.
- 판단의 한계
- 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다.
두 번째 park와 생존 조회
- 가능한 진행
- 두 번째 park도 임의로 반환할 수 있습니다.
- 판단의 한계
- 출력에 도달해도
wrong-second-permit은 false 또는 true일 수 있습니다.
false는 조회 시 작업자가 살아 있다는 뜻이며 두 번째 park에 이미 들어갔다는 증거가 아닙니다. 정리용 interrupt는 출력 뒤에 있으므로 앞선 래치 대기에서 멈춘 경로를 구제하지 못합니다.
이 예의 조건
- 두 unpark는 최대 한 허가증입니다. 내부 mayPark가 그 허가증을 소비할 수 있습니다.
- 첫 명시적 park의 대기와 반환은 대안이며 park 임의 반환도 가능합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 선행 unpark 두 번은 허가증 하나입니다. mayPark 내부 대기가 소비하면 첫 명시적 park와 main 래치가 멈출 수 있으며 첫 park 반환 뒤에도 두 번째 park의 임의 반환 때문에 출력은 고정되지 않습니다. |
| 5 | 허가증은 최대 1개 |
| 19 | 각 영역은 가능한 경로 |
| 20 | 고정 출력 아님 |
| 구분 | 내용 |
|---|---|
| 설명 | 선행 unpark 두 번은 허가증 하나입니다. mayPark 내부 대기가 소비하면 첫 명시적 park와 main 래치가 멈출 수 있으며 첫 park 반환 뒤에도 두 번째 park의 임의 반환 때문에 출력은 고정되지 않습니다. |
| 전체 읽기 정보 | 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 DoubleUnparkPermitBug의 래치 대기, 첫 park 뒤 신호, 두 번째 park와 생존 조회를 실제 순서로 읽고 관측 한계를 구분합니다. PARK · 허가증과 관측 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 unpark 허가증은 쌓이지 않습니다. 중간 래치 대기가 소비할 수 있어 첫 park 뒤 출력도 보장되지 않습니다. mayPark 래치 대기 가능한 진행 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다. 판단의 한계 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다. 첫 park 뒤 firstReturned 신호 가능한 진행 첫 park가 반환해야 신호를 보냅니다. 판단의 한계 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다. 두 번째 park와 생존 조회 가능한 진행 두 번째 park도 임의로 반환할 수 있습니다. 판단의 한계 출력에 도달해도 wrong-second-permit 은 false 또는 true일 수 있습니다. false는 조회 시 작업자가 살아 있다는 뜻이며 두 번째 park에 이미 들어갔다는 증거가 아닙니다. 정리용 interrupt는 출력 뒤에 있으므로 앞선 래치 대기에서 멈춘 경로를 구제하지 못합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공통 선행 조건 main이 worker 시작 전에 unpark를 두 번 수행합니다. |
|
|
| 첫 명시적 park의 두 조건 mayPark 이후 첫 명시적 park가 아직 대기 중인지 반환했는지 비교합니다. |
|
|
| 각 조건의 후속 범위 첫 park 미반환과 반환 이후의 관찰을 함께 둡니다. |
|
|
두 번의 unpark만으로 두 park의 결과를 고정할 수 없다
선행 unpark 두 번도 허가증 최대 1이며 내부 대기가 소비할 수 있는 두 가능한 경로입니다. 조건부 소비·대기·관측을 단계로 강조하되 고정 출력이나 두 허가증을 주장하지 않습니다.
DoubleUnparkPermitBug의 래치 대기, 첫 park 뒤 신호, 두 번째 park와 생존 조회를 실제 순서로 읽고 관측 한계를 구분합니다.
PARK · 허가증과 관측
두 번의 unpark만으로 두 park의 결과를 고정할 수 없다
unpark 허가증은 쌓이지 않습니다. 중간 래치 대기가 소비할 수 있어 첫 park 뒤 출력도 보장되지 않습니다.
mayPark래치 대기- 가능한 진행
- 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다.
- 판단의 한계
- 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다.
첫 park 뒤
firstReturned신호- 가능한 진행
- 첫 park가 반환해야 신호를 보냅니다.
- 판단의 한계
- 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다.
두 번째 park와 생존 조회
- 가능한 진행
- 두 번째 park도 임의로 반환할 수 있습니다.
- 판단의 한계
- 출력에 도달해도
wrong-second-permit은 false 또는 true일 수 있습니다.
false는 조회 시 작업자가 살아 있다는 뜻이며 두 번째 park에 이미 들어갔다는 증거가 아닙니다. 정리용 interrupt는 출력 뒤에 있으므로 앞선 래치 대기에서 멈춘 경로를 구제하지 못합니다.
이 예의 조건
- 두 unpark는 최대 한 허가증입니다. 내부 mayPark가 그 허가증을 소비할 수 있습니다.
- 첫 명시적 park의 대기와 반환은 대안이며 park 임의 반환도 가능합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 선행 unpark 두 번은 허가증 하나입니다. mayPark 내부 대기가 소비하면 첫 명시적 park와 main 래치가 멈출 수 있으며 첫 park 반환 뒤에도 두 번째 park의 임의 반환 때문에 출력은 고정되지 않습니다. |
| 5 | 허가증은 최대 1개 |
| 19 | 각 영역은 가능한 경로 |
| 20 | 고정 출력 아님 |
| 구분 | 내용 |
|---|---|
| 설명 | 선행 unpark 두 번은 허가증 하나입니다. mayPark 내부 대기가 소비하면 첫 명시적 park와 main 래치가 멈출 수 있으며 첫 park 반환 뒤에도 두 번째 park의 임의 반환 때문에 출력은 고정되지 않습니다. |
| 전체 읽기 정보 | 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 DoubleUnparkPermitBug의 래치 대기, 첫 park 뒤 신호, 두 번째 park와 생존 조회를 실제 순서로 읽고 관측 한계를 구분합니다. PARK · 허가증과 관측 두 번의 unpark만으로 두 park의 결과를 고정할 수 없다 unpark 허가증은 쌓이지 않습니다. 중간 래치 대기가 소비할 수 있어 첫 park 뒤 출력도 보장되지 않습니다. mayPark 래치 대기 가능한 진행 래치 내부의 대기가 선행 허가증을 소비할 수 있습니다. 판단의 한계 허가증이 첫 번째 명시적 park까지 남는다고 보장하지 않습니다. 첫 park 뒤 firstReturned 신호 가능한 진행 첫 park가 반환해야 신호를 보냅니다. 판단의 한계 반환하지 않으면 main도 래치에서 계속 기다릴 수 있습니다. 두 번째 park와 생존 조회 가능한 진행 두 번째 park도 임의로 반환할 수 있습니다. 판단의 한계 출력에 도달해도 wrong-second-permit 은 false 또는 true일 수 있습니다. false는 조회 시 작업자가 살아 있다는 뜻이며 두 번째 park에 이미 들어갔다는 증거가 아닙니다. 정리용 interrupt는 출력 뒤에 있으므로 앞선 래치 대기에서 멈춘 경로를 구제하지 못합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공통 선행 조건 main이 worker 시작 전에 unpark를 두 번 수행합니다. |
|
|
| 첫 명시적 park의 두 조건 mayPark 이후 첫 명시적 park가 아직 대기 중인지 반환했는지 비교합니다. |
|
|
| 각 조건의 후속 범위 첫 park 미반환과 반환 이후의 관찰을 함께 둡니다. |
|
|
두 신호를 보존해야 한다면 Semaphore 허가증, BlockingQueue 항목, CountDownLatch count처럼 수량을 표현하는 추상화를 사용합니다.
LockSupport 허가증을 이벤트 카운터로 쓰면 신호가 합쳐져 작업이 멈춥니다.
원 DoubleUnparkPermitBug에서는 mayPark.await 자체가 내부 대기를 할 수 있습니다. 그 대기가 선행 허가증을 소비하면 첫 명시적 park에서 멈춰 firstReturned 신호도 보내지 못합니다. 출력 뒤 interrupt는 이 앞선 멈춤을 정리하지 못합니다. 따라서 이 원 프로그램을 두 번째 park가 반드시 멈추는 검증기로 사용하지 마세요. 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();
}
}게이트의 조건 공개와 대기 해제를 구분한다
open을 먼저 공개한 경로와 검사 직후 공개한 경로에서 등록·조건 재검사·unpark 역할이 다릅니다. 조건과 허가증을 구분하는 실제 전후 과정입니다.
ParkedConditionGate의 open 쓰기, 조건 반복, 인터럽트 처리와 성공 반환을 실제 코드로 비교합니다.
게이트 · 조건과 깨우기
게이트의 조건 공개와 대기 해제를 구분한다
open=true를 먼저 공개하고 unpark를 요청합니다. 대기자는 반환할 때 조건과 중단 여부를 다시 확인합니다.
open() 호출
- 조건·신호 처리
- volatile
open을 true로 쓴 뒤 등록된 대기자를 unpark합니다. - 이어지는 동작
- 조건 공개가 대기 해제 요청보다 먼저입니다.
while (!open) 안
- 조건·신호 처리
- park 뒤
Thread.interrupted()를 검사합니다. - 이어지는 동작
- true이면 플래그를 지우고 예외를 던집니다.
open이 true인 검사
- 조건·신호 처리
- 반복문을 건너뛰거나 빠져나옵니다.
- 이어지는 동작
- 호출자가
gate-open을 출력합니다.
원문의 main은 interrupt를 보내지 않습니다. 또 WAITING 관측을 기다리는 반복문에 시간 제한이 없으므로 이 예제를 종료 시간이 정해진 조정 도구로 볼 수 없습니다.
이 예의 조건
- 단일 대기자 예제이며 관측 폴링에는 상한이 없습니다. 먼저 열린 조건과 검사 직후 열린 조건은 다른 스케줄입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 조건 공개가 먼저인 경우는 waiter가 null이어도 while에서 열림을 읽어 대기를 건너뜁니다. false 검사 직후 공개되면 이미 등록된 작업자를 unpark하며 원래 단일 대기자 취소 정책을 그대로 둡니다. |
| 7 | 작업자 등록·조건 검사 |
| 10 | waiter 등록·false 확인 |
| 14 | park 반환 뒤 다시 검사 |
| 18 | 단일 대기자·관측 폴링 상한 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | 조건 공개가 먼저인 경우는 waiter가 null이어도 while에서 열림을 읽어 대기를 건너뜁니다. false 검사 직후 공개되면 이미 등록된 작업자를 unpark하며 원래 단일 대기자 취소 정책을 그대로 둡니다. |
| 전체 읽기 정보 | 게이트의 조건 공개와 대기 해제를 구분한다 게이트의 조건 공개와 대기 해제를 구분한다 ParkedConditionGate의 open 쓰기, 조건 반복, 인터럽트 처리와 성공 반환을 실제 코드로 비교합니다. 게이트 · 조건과 깨우기 게이트의 조건 공개와 대기 해제를 구분한다 open=true를 먼저 공개하고 unpark를 요청합니다. 대기자는 반환할 때 조건과 중단 여부를 다시 확인합니다. open() 호출 조건·신호 처리 volatile open 을 true로 쓴 뒤 등록된 대기자를 unpark합니다. 이어지는 동작 조건 공개가 대기 해제 요청보다 먼저입니다. while (!open) 안 조건·신호 처리 park 뒤 Thread.interrupted() 를 검사합니다. 이어지는 동작 true이면 플래그를 지우고 예외를 던집니다. open 이 true인 검사 조건·신호 처리 반복문을 건너뛰거나 빠져나옵니다. 이어지는 동작 호출자가 gate-open 을 출력합니다. 원문의 main은 interrupt를 보내지 않습니다. 또 WAITING 관측을 기다리는 반복문에 시간 제한이 없으므로 이 예제를 종료 시간이 정해진 조정 도구로 볼 수 없습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 대기자 등록 worker가 waiter에 자기 참조를 등록합니다. |
|
|
| 조건 검사와 공개 worker의 open 검사 시점에서 두 스케줄을 비교합니다. |
|
|
| 진행 또는 재검사 열린 조건은 진행하고 park 반환 경로는 조건·중단 정책을 다시 봅니다. |
|
|
게이트의 조건 공개와 대기 해제를 구분한다
open을 먼저 공개한 경로와 검사 직후 공개한 경로에서 등록·조건 재검사·unpark 역할이 다릅니다. 조건과 허가증을 구분하는 실제 전후 과정입니다.
ParkedConditionGate의 open 쓰기, 조건 반복, 인터럽트 처리와 성공 반환을 실제 코드로 비교합니다.
게이트 · 조건과 깨우기
게이트의 조건 공개와 대기 해제를 구분한다
open=true를 먼저 공개하고 unpark를 요청합니다. 대기자는 반환할 때 조건과 중단 여부를 다시 확인합니다.
open() 호출
- 조건·신호 처리
- volatile
open을 true로 쓴 뒤 등록된 대기자를 unpark합니다. - 이어지는 동작
- 조건 공개가 대기 해제 요청보다 먼저입니다.
while (!open) 안
- 조건·신호 처리
- park 뒤
Thread.interrupted()를 검사합니다. - 이어지는 동작
- true이면 플래그를 지우고 예외를 던집니다.
open이 true인 검사
- 조건·신호 처리
- 반복문을 건너뛰거나 빠져나옵니다.
- 이어지는 동작
- 호출자가
gate-open을 출력합니다.
원문의 main은 interrupt를 보내지 않습니다. 또 WAITING 관측을 기다리는 반복문에 시간 제한이 없으므로 이 예제를 종료 시간이 정해진 조정 도구로 볼 수 없습니다.
이 예의 조건
- 단일 대기자 예제이며 관측 폴링에는 상한이 없습니다. 먼저 열린 조건과 검사 직후 열린 조건은 다른 스케줄입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 조건 공개가 먼저인 경우는 waiter가 null이어도 while에서 열림을 읽어 대기를 건너뜁니다. false 검사 직후 공개되면 이미 등록된 작업자를 unpark하며 원래 단일 대기자 취소 정책을 그대로 둡니다. |
| 7 | 작업자 등록·조건 검사 |
| 10 | waiter 등록·false 확인 |
| 14 | park 반환 뒤 다시 검사 |
| 18 | 단일 대기자·관측 폴링 상한 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | 조건 공개가 먼저인 경우는 waiter가 null이어도 while에서 열림을 읽어 대기를 건너뜁니다. false 검사 직후 공개되면 이미 등록된 작업자를 unpark하며 원래 단일 대기자 취소 정책을 그대로 둡니다. |
| 전체 읽기 정보 | 게이트의 조건 공개와 대기 해제를 구분한다 게이트의 조건 공개와 대기 해제를 구분한다 ParkedConditionGate의 open 쓰기, 조건 반복, 인터럽트 처리와 성공 반환을 실제 코드로 비교합니다. 게이트 · 조건과 깨우기 게이트의 조건 공개와 대기 해제를 구분한다 open=true를 먼저 공개하고 unpark를 요청합니다. 대기자는 반환할 때 조건과 중단 여부를 다시 확인합니다. open() 호출 조건·신호 처리 volatile open 을 true로 쓴 뒤 등록된 대기자를 unpark합니다. 이어지는 동작 조건 공개가 대기 해제 요청보다 먼저입니다. while (!open) 안 조건·신호 처리 park 뒤 Thread.interrupted() 를 검사합니다. 이어지는 동작 true이면 플래그를 지우고 예외를 던집니다. open 이 true인 검사 조건·신호 처리 반복문을 건너뛰거나 빠져나옵니다. 이어지는 동작 호출자가 gate-open 을 출력합니다. 원문의 main은 interrupt를 보내지 않습니다. 또 WAITING 관측을 기다리는 반복문에 시간 제한이 없으므로 이 예제를 종료 시간이 정해진 조정 도구로 볼 수 없습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 대기자 등록 worker가 waiter에 자기 참조를 등록합니다. |
|
|
| 조건 검사와 공개 worker의 open 검사 시점에서 두 스케줄을 비교합니다. |
|
|
| 진행 또는 재검사 열린 조건은 진행하고 park 반환 경로는 조건·중단 정책을 다시 봅니다. |
|
|
이 단일 대기자 예제는 교육용입니다.
이 코드의 중단 정책은 park를 거친 경로에 있습니다. Thread.interrupted()는 현재 플래그를 읽고 지우므로, 이 검사에서 true를 읽으면 플래그가 지워진 채 예외로 종료합니다. 이미 open=true여서 반복문에 들어가지 않은 호출은 이 검사를 수행하지 않으며, 기존 인터럽트 플래그도 그대로 남습니다. 동시에 열림과 중단 요청이 들어오면 항상 열림을 우선하는 API라고도 할 수 없습니다. 인터럽트 가능 대기 API를 만들 때는 진입 시 검사·플래그 복구·대기자 등록 해제까지 정책을 따로 정해야 합니다.
여러 대기자, 시간 제한, 취소, 큐에서의 제거를 직접 구현하면 검증 부담이 급격히 커지므로 운영에서는 동기화 도구와 동시성 컬렉션을 우선합니다.
open이 waiter 등록보다 먼저 끝나면 open은 깨울 대상을 찾지 못할 수 있습니다. 그래도 작업자는 등록 뒤 while 조건에서 이미 true인 open을 읽고 대기를 건너뜁니다. 반대로 조건을 false로 읽은 직후 open이 실행되면 등록된 작업자에게 unpark를 보냅니다. 두 경우를 나눠 따라가면 “대상 참조 등록 → 조건 검사 → 대기” 순서와 “조건 쓰기 → 대상 깨우기” 순서의 이유가 보입니다. 원 main의 WAITING 폴링은 별도 실험용 관측이며 업무 조건 대신 사용할 수 없습니다.
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");
}
}큐의 소유권과 대기·종료 호출의 위치가 다르다
submit/consume/close가 jobs 모니터 안 상태 변경 뒤 밖에서 unpark/park/처리/join을 합니다. 같은 큐와 종료 조건을 고정해 락·대기 경계를 추적하기 좋습니다.
ParkedJobDispatcher의 submit, consume, close가 jobs 모니터 안에서 다루는 상태와 모니터 밖에서 하는 호출을 비교합니다.
디스패처 · 락 안과 밖
큐의 소유권과 대기·종료 호출의 위치가 다르다
jobs 모니터는 큐와 종료 상태를 보호합니다. park·unpark·join은 그 밖에서 대기와 회수를 맡습니다.
submit
- jobs 모니터 안
- 종료 여부를 확인하고 작업을 큐에 넣습니다.
- 모니터 밖
- 작업자를 unpark합니다.
consume
- jobs 모니터 안
- 작업을 꺼내고, 비었으며 닫혔으면 반환합니다.
- 모니터 밖
- 비었으면
park(this)뒤 재검사하고, 작업이 있으면 로그를 출력합니다.
close
- jobs 모니터 안
closed를 true로 바꿉니다.- 모니터 밖
- unpark 뒤 join하여 작업자 종료를 기다립니다.
상태를 보호하는 모니터는 jobs이고 park의 진단 객체는 this입니다. 외부 interrupt를 처리하지 않으므로 빈 큐에서 반복 실행할 수 있지만 원문의 main은 이 경로를 실행하지 않습니다.
이 예의 조건
- submit·consume·close는 메서드별 경계 비교이며 세 호출을 차례로 완료했다는 흐름이 아닙니다.
- jobs는 모니터, this는 park 진단 객체입니다. 외부 interrupt 취소는 처리하지 않고 start 전 close의 종료를 보장하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | submit·consume·close의 jobs 모니터 안의 상태 판단과 밖의 unpark/park/join을 각 메서드 경로로 구분합니다. blocker인 this와 모니터인 jobs는 서로 다릅니다. |
| 5 | closed 검사 |
| 6 | jobs.add |
| 11 | poll |
| 12 | 큐가 비고 닫혔으면 종료 |
| 14 | 빈 큐면 park 뒤 재검사 |
| 15 | 항목이 있으면 콘솔 handled |
| 20 | unpark |
| 21 | 정상 join |
| 23 | this는 park 진단 객체 |
| 24 | 락은 jobs |
| 25 | 외부 interrupt 취소는 원래 코드에 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | submit·consume·close의 jobs 모니터 안의 상태 판단과 밖의 unpark/park/join을 각 메서드 경로로 구분합니다. blocker인 this와 모니터인 jobs는 서로 다릅니다. |
| 전체 읽기 정보 | 큐의 소유권과 대기·종료 호출의 위치가 다르다 큐의 소유권과 대기·종료 호출의 위치가 다르다 ParkedJobDispatcher의 submit, consume, close가 jobs 모니터 안에서 다루는 상태와 모니터 밖에서 하는 호출을 비교합니다. 디스패처 · 락 안과 밖 큐의 소유권과 대기·종료 호출의 위치가 다르다 jobs 모니터는 큐와 종료 상태를 보호합니다. park·unpark·join은 그 밖에서 대기와 회수를 맡습니다. submit jobs 모니터 안 종료 여부를 확인하고 작업을 큐에 넣습니다. 모니터 밖 작업자를 unpark합니다. consume jobs 모니터 안 작업을 꺼내고, 비었으며 닫혔으면 반환합니다. 모니터 밖 비었으면 park(this) 뒤 재검사하고, 작업이 있으면 로그를 출력합니다. close jobs 모니터 안 closed 를 true로 바꿉니다. 모니터 밖 unpark 뒤 join하여 작업자 종료를 기다립니다. 상태를 보호하는 모니터는 jobs이고 park의 진단 객체는 this입니다. 외부 interrupt를 처리하지 않으므로 빈 큐에서 반복 실행할 수 있지만 원문의 main은 이 경로를 실행하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 모니터 진입과 판정 세 메서드가 각 호출에서 jobs 모니터를 획득해 상태를 판정합니다. |
|
|
| 보호된 상태 접근 각 메서드의 읽기·변경을 모니터 안에서 수행합니다. |
|
|
| 모니터 밖 진행 모니터를 반납한 뒤 각 호출의 후속을 수행합니다. |
|
|
큐의 소유권과 대기·종료 호출의 위치가 다르다
submit/consume/close가 jobs 모니터 안 상태 변경 뒤 밖에서 unpark/park/처리/join을 합니다. 같은 큐와 종료 조건을 고정해 락·대기 경계를 추적하기 좋습니다.
ParkedJobDispatcher의 submit, consume, close가 jobs 모니터 안에서 다루는 상태와 모니터 밖에서 하는 호출을 비교합니다.
디스패처 · 락 안과 밖
큐의 소유권과 대기·종료 호출의 위치가 다르다
jobs 모니터는 큐와 종료 상태를 보호합니다. park·unpark·join은 그 밖에서 대기와 회수를 맡습니다.
submit
- jobs 모니터 안
- 종료 여부를 확인하고 작업을 큐에 넣습니다.
- 모니터 밖
- 작업자를 unpark합니다.
consume
- jobs 모니터 안
- 작업을 꺼내고, 비었으며 닫혔으면 반환합니다.
- 모니터 밖
- 비었으면
park(this)뒤 재검사하고, 작업이 있으면 로그를 출력합니다.
close
- jobs 모니터 안
closed를 true로 바꿉니다.- 모니터 밖
- unpark 뒤 join하여 작업자 종료를 기다립니다.
상태를 보호하는 모니터는 jobs이고 park의 진단 객체는 this입니다. 외부 interrupt를 처리하지 않으므로 빈 큐에서 반복 실행할 수 있지만 원문의 main은 이 경로를 실행하지 않습니다.
이 예의 조건
- submit·consume·close는 메서드별 경계 비교이며 세 호출을 차례로 완료했다는 흐름이 아닙니다.
- jobs는 모니터, this는 park 진단 객체입니다. 외부 interrupt 취소는 처리하지 않고 start 전 close의 종료를 보장하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | submit·consume·close의 jobs 모니터 안의 상태 판단과 밖의 unpark/park/join을 각 메서드 경로로 구분합니다. blocker인 this와 모니터인 jobs는 서로 다릅니다. |
| 5 | closed 검사 |
| 6 | jobs.add |
| 11 | poll |
| 12 | 큐가 비고 닫혔으면 종료 |
| 14 | 빈 큐면 park 뒤 재검사 |
| 15 | 항목이 있으면 콘솔 handled |
| 20 | unpark |
| 21 | 정상 join |
| 23 | this는 park 진단 객체 |
| 24 | 락은 jobs |
| 25 | 외부 interrupt 취소는 원래 코드에 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | submit·consume·close의 jobs 모니터 안의 상태 판단과 밖의 unpark/park/join을 각 메서드 경로로 구분합니다. blocker인 this와 모니터인 jobs는 서로 다릅니다. |
| 전체 읽기 정보 | 큐의 소유권과 대기·종료 호출의 위치가 다르다 큐의 소유권과 대기·종료 호출의 위치가 다르다 ParkedJobDispatcher의 submit, consume, close가 jobs 모니터 안에서 다루는 상태와 모니터 밖에서 하는 호출을 비교합니다. 디스패처 · 락 안과 밖 큐의 소유권과 대기·종료 호출의 위치가 다르다 jobs 모니터는 큐와 종료 상태를 보호합니다. park·unpark·join은 그 밖에서 대기와 회수를 맡습니다. submit jobs 모니터 안 종료 여부를 확인하고 작업을 큐에 넣습니다. 모니터 밖 작업자를 unpark합니다. consume jobs 모니터 안 작업을 꺼내고, 비었으며 닫혔으면 반환합니다. 모니터 밖 비었으면 park(this) 뒤 재검사하고, 작업이 있으면 로그를 출력합니다. close jobs 모니터 안 closed 를 true로 바꿉니다. 모니터 밖 unpark 뒤 join하여 작업자 종료를 기다립니다. 상태를 보호하는 모니터는 jobs이고 park의 진단 객체는 this입니다. 외부 interrupt를 처리하지 않으므로 빈 큐에서 반복 실행할 수 있지만 원문의 main은 이 경로를 실행하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 모니터 진입과 판정 세 메서드가 각 호출에서 jobs 모니터를 획득해 상태를 판정합니다. |
|
|
| 보호된 상태 접근 각 메서드의 읽기·변경을 모니터 안에서 수행합니다. |
|
|
| 모니터 밖 진행 모니터를 반납한 뒤 각 호출의 후속을 수행합니다. |
|
|
close는 시작된 작업자의 큐를 비운 뒤 종료를 기다립니다. 이 main은 start → submit → close 순서로 사용하며, 소비자는 closed여도 꺼낼 항목이 있으면 처리하고 다음 검사에서 큐가 비었을 때 반환합니다. closed가 volatile이 아니어도 모든 읽기·쓰기가 같은 jobs 모니터 안에 있어 공개됩니다. close의 정상 join은 작업자 종료를 확인하지만 시작 전 close까지 처리 완료로 보장하지는 않습니다. 또 원 소비자는 인터럽트 플래그를 처리하지 않아 빈 큐에서 park가 즉시 반환하는 반복이 생길 수 있습니다. 외부 interrupt를 종료 명령으로 사용할 구현은 아니며, 본 예제의 종료 요청은 closed 조건과 unpark입니다.
BlockingQueue가 이미 이 요구를 더 완전하게 제공하므로 직접 구현은 원리 이해 용도입니다.
park를 직접 쓸지 판단하는 기준
락, 세마포어, 래치, 블로킹 큐로 요구를 표현할 수 있으면 그 API를 사용합니다.
LockSupport는 동기화 도구 구현, 실행 시점 스케줄러, 특수 저수준 라이브러리처럼 조건 큐와 취소를 직접 소유해야 할 때 검토합니다.
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의 허가증 소비를 따로 입증하지는 않습니다.
여러 번의 업무 이벤트를 표현하려면 큐 또는 세마포어로 바꿉니다.
검산할 사실은 observed-ready 한 번과 작업자 종료입니다. ready=true는 준비 사실을 보관하고, unpark 호출 횟수는 완료한 업무 개수가 아닙니다. 다음 변형에서는 main이 ready를 먼저 정한 뒤 작업자를 시작해도 반복문이 park를 건너뛰는 이유를 설명하세요. 이 변형도 start의 값 공개가 있으므로 volatile 하나의 효과를 따로 측정한 실험이라고 부르지는 않습니다.
LockSupport의 핵심은 허가증 하나가 선행 unpark를 기억하되 여러 신호를 누적하지 않는다는 점입니다.
park 반환 뒤 조건을 재검사하고 blocker로 원인을 남기며, 가능한 경우 더 높은 수준의 동기화 도구를 선택해야 합니다.
추가 연습 — 신호, 조건, 종료를 나누어 읽습니다. 모든 스레드가 시작되고 필요한 호출이 정상 수행되는 조건입니다. (1) 여러 작업 항목 3개를 큐에 넣은 뒤 작업자에게 unpark를 세 번 보냅니다. 대기 허가증의 최대 수와 큐에서 처리할 항목 수는 각각 얼마인가요? 두 번째 park의 반환만으로 허가증 2개가 저장되어 있었다고 결론 낼 수 있나요? (2) ParkedConditionGate의 open을 작업자 진입보다 먼저 호출합니다. 작업자가 이미 인터럽트된 상태로 awaitOpen에 들어오면 이 코드가 예외를 던지고 플래그를 지우나요? 반대로 open=false인 상태에서 이미 인터럽트된 작업자가 반복문 안 park를 호출하면 어떤 경로인가요? (3) Dispatcher를 한 번 start한 뒤 index·publish를 차례로 submit하고 즉시 close합니다. 콘솔 처리 두 줄의 순서와 dispatcher-closed의 위치를 구하고, 큐가 비었을 때 아무 조건 변경 없이 unpark만 보낸 경우도 설명하세요.
추가 연습의 풀이 확인
- 허가증은 최대 1개이고, 성공적으로 큐에 저장한 업무 항목은 3개입니다. 작업 수는 큐가 보관하므로 세 unpark가 합쳐져도 항목 수를 잃지 않습니다. park는 인터럽트나 임의 반환으로도 끝날 수 있으므로 반환 횟수로 저장된 허가증 개수를 역산하지 않습니다.
- open=true를 읽으면 while을 건너뛰어 예외도 Thread.interrupted 검사도 수행하지 않습니다. 기존 플래그는 true로 남습니다. open=false인 경우 park는 인터럽트 때문에 즉시 반환할 수 있고, 뒤의 Thread.interrupted가 true를 읽으며 지운 뒤 IllegalStateException을 던집니다. 이 예제의 성공·취소 정책은 두 경로를 나누어 설명해야 합니다.
- 같은 main이 큐에 index 다음 publish를 넣고 단일 소비자가 FIFO로 꺼내므로 handled=index 다음 handled=publish입니다. close는 정상 join을 거쳐 dispatcher-closed를 그 뒤에 출력합니다. closed=true여도 이미 수락한 두 항목을 먼저 비웁니다. 빈 큐·closed=false에서 무관한 unpark가 오면 다음 반복에서 다시 그 조건을 검사하고 기다립니다. unpark 자체가 업무나 종료 조건을 만들어 주지는 않습니다.