스레드 정보와 생명 주기
id·이름·우선순위·그룹을 관찰하고 NEW부터 TERMINATED까지 상태 전이를 제어된 사건으로 재현합니다.
Thread 객체는 작업뿐 아니라 식별 정보와 현재 생명 주기 정보를 제공합니다.
이 값은 관찰과 진단에 유용하지만 업무 순서를 보장하는 제어 장치로 쓰면 안 됩니다.
상태 이름이 어떤 대기 원인을 뜻하는지 실제 사건과 연결해야 스레드 덤프도 읽을 수 있습니다.
run 재정의의 검사 예외 제한
Runnable.run()과 Thread.run()은 검사 예외를 선언하지 않습니다.
override 메서드는 부모보다 넓은 검사 예외를 새로 던질 수 없으므로 아래 코드는 실제 컴파일 오류입니다.
public final class CheckedExceptionRunFailure extends Thread {
@Override
public void run() throws InterruptedException {
Thread.sleep(10);
}
}스레드 접점에서는 InterruptedException을 catch해 중단 정책을 수행해야 합니다.
단순히 RuntimeException으로 감싸면 협력적 취소 의미를 잃을 수 있습니다.
복구할 수 없으면 상태를 정리하고 interrupt 플래그를 복원하거나 결과 추상화로 실패를 전달합니다.
스레드 ID·이름과 업무 키
threadId()는 생성할 때 부여되는 양수이며 해당 스레드의 수명 동안 고유하고 바뀌지 않습니다. 재실행 후에도 유지되는 영구 업무 식별자는 아닙니다.
이 문서의 생성자로 만드는 플랫폼 스레드는 이름을 지정하지 않으면 Thread-N 계열 기본 이름을 받습니다.
운영 로그에는 역할이 드러나는 이름을 부여하되 주문 번호 같은 업무 키는 별도 구조화 필드로 남깁니다.
public final class ThreadMetadataReport {
public static void main(String[] args) throws InterruptedException {
ThreadGroup group = new ThreadGroup("import-workers");
Thread worker = new Thread(group, () -> report("inside"), "import-csv");
report("main-before");
System.out.println(
"new-state=" + worker.getState() + ", group=" + worker.getThreadGroup().getName());
worker.start();
worker.join();
System.out.println("end-state=" + worker.getState() + ", alive=" + worker.isAlive());
}
private static void report(String point) {
Thread t = Thread.currentThread();
System.out.println(
point
+ " id="
+ t.threadId()
+ ", name="
+ t.getName()
+ ", priority="
+ t.getPriority()
+ ", daemon="
+ t.isDaemon());
}
}ThreadMetadataReport는 report 안에서 현재 스레드를 조회하고, main에서는 작업자의 시작 전 상태와 그룹 및 join 뒤 종료 상태를 조회합니다.
| 원문의 조회 지점 | 조회 대상 | 출력에서 구분할 정보 |
|---|---|---|
report("main-before") | 현재 실행 중인 main | id / name / priority / daemon |
시작 전 worker.getState()·그룹 조회 | worker 객체 | new-state=NEW, group=import-workers |
report("inside") | 실행 중인 import-csv | id / name / priority / daemon |
worker.join() 뒤 조회 | worker 객체 | end-state=TERMINATED, alive=false |
report("main-before")- 조회 대상: 현재 실행 중인
main출력에서 구분할 정보:id / name / priority / daemon - 시작 전
worker.getState()·그룹 조회 - 조회 대상:
worker객체출력에서 구분할 정보:new-state=NEW, group=import-workers report("inside")- 조회 대상: 실행 중인
import-csv출력에서 구분할 정보:id / name / priority / daemon worker.join()뒤 조회- 조회 대상:
worker객체출력에서 구분할 정보:end-state=TERMINATED, alive=false
report는 Thread.currentThread()를 읽습니다. 원문은 NEW인 작업자의 id를 main에서 출력하지 않습니다. id와 우선순위는 업무 식별자나 완료 순서의 보장이 아닙니다.
API상 NEW인 작업자의 id와 이름도 조회할 수 있습니다.
종료된 Thread의 그룹 참조는 null이 될 수 있으므로 그룹은 시작 전에 확인했습니다.
ThreadGroup은 오래된 그룹화 API이고 새 설계의 자원 관리 수단으로는 실행기와 명시적 소유자를 우선합니다.
스레드 상태와 대기 원인
RUNNABLE은 CPU에서 실제 명령을 수행하는 순간뿐 아니라 실행 가능 큐에서 차례를 기다리는 상태도 포함합니다.
BLOCKED는 synchronized 모니터 진입 대기입니다.
WAITING은 시간 제한 없이 다른 사건을, TIMED_WAITING은 제한 시간과 함께 기다립니다.
아래 프로그램은 래치로 WAITING, sleep(500)으로 TIMED_WAITING, main이 가진 모니터로 BLOCKED 관측을 시도합니다.
import java.util.concurrent.CountDownLatch;
public final class ControlledThreadStates {
public static void main(String[] args) throws InterruptedException {
CountDownLatch waitingGate = new CountDownLatch(1);
Object monitor = new Object();
Thread waiting = new Thread(() -> await(waitingGate), "state-waiting");
Thread timed =
new Thread(
() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
},
"state-timed");
Thread blocked =
new Thread(
() -> {
synchronized (monitor) {
System.out.println("blocked-entered");
}
},
"state-blocked");
waiting.start();
timed.start();
synchronized (monitor) {
blocked.start();
waitFor(waiting, Thread.State.WAITING);
waitFor(timed, Thread.State.TIMED_WAITING);
waitFor(blocked, Thread.State.BLOCKED);
System.out.println(
waiting.getState() + "," + timed.getState() + "," + blocked.getState());
}
waitingGate.countDown();
waiting.join();
timed.join();
blocked.join();
}
private static void await(CountDownLatch gate) {
try {
gate.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void waitFor(Thread t, Thread.State target) throws InterruptedException {
for (int i = 0; i < 200 && t.getState() != target; i++) {
Thread.sleep(1);
}
if (t.getState() != target)
throw new IllegalStateException(t.getName() + "=" + t.getState());
}
}ControlledThreadStates는 래치 대기, 500밀리초 sleep, main이 보유한 모니터 진입을 각각 관측하려 합니다. 시간 대기는 출력 전에 끝날 수 있고 관측 실패 시 래치가 해제되지 않습니다.
| 목표 상태·작업자 | 대기를 만드는 원문 | 유지 조건과 관측 한계 |
|---|---|---|
WAITINGstate-waiting | waitingGate.await() | 래치가 0이 될 때까지 기다림; countDown() 전에는 대기 원인이 유지됨 |
TIMED_WAITINGstate-timed | Thread.sleep(500) | 시간 경과나 interrupt로 대기에서 벗어남; 폴링 뒤 출력 시점까지 유지된다는 보장은 없음 |
BLOCKEDstate-blocked | synchronized (monitor) | main이 모니터를 가진 동안 진입 대기; 모니터 해제 뒤 진입 가능 |
WAITINGstate-waiting- 대기를 만드는 원문:
waitingGate.await()유지 조건과 관측 한계: 래치가 0이 될 때까지 기다림;countDown()전에는 대기 원인이 유지됨 TIMED_WAITINGstate-timed- 대기를 만드는 원문:
Thread.sleep(500)유지 조건과 관측 한계: 시간 경과나 interrupt로 대기에서 벗어남; 폴링 뒤 출력 시점까지 유지된다는 보장은 없음 BLOCKEDstate-blocked- 대기를 만드는 원문:
synchronized (monitor)유지 조건과 관측 한계:main이 모니터를 가진 동안 진입 대기; 모니터 해제 뒤 진입 가능
waitFor는 목표 상태를 관측했는지 확인합니다. 세 상태 조회는 하나의 원자적 스냅샷이 아니며, 관측 실패로 예외가 나면 래치를 해제하지 않아 비데몬 대기자가 남을 수 있습니다.
상태를 변경하는 원인은 상태 조회가 아니라 사건의 완료입니다.
스레드 생명 주기의 비가역성
start는 NEW인 스레드의 실행을 요청합니다. 실행 중 여러 대기 상태를 거칠 수 있지만, 호출 직후 getState()로 RUNNABLE이 반드시 관측되는 것은 아닙니다.
run()이 정상 반환하거나, 처리되지 않은 예외 뒤 예외 처리기까지 끝나면 TERMINATED입니다.
TERMINATED 객체를 다시 start할 수 없습니다.
isAlive()는 start 이후 종료 전까지 true지만 작업이 유용하게 진행 중이라는 뜻은 아닙니다.
락 교착 상태나 무한 WAITING도 생존 상태입니다.
상태 확인에는 큐 진척, 마지막 완료 시각, 시간 제한 같은 애플리케이션 신호를 함께 사용합니다.
처리되지 않은 예외로 끝난 스레드도 TERMINATED입니다.
getState()만 보면 성공·실패를 구별할 수 없으므로 처리기나 Future 결과로 원인을 기록합니다.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class LifecycleOutcomeRegistry {
public static void main(String[] args) throws InterruptedException {
Map<String, String> outcomes = new ConcurrentHashMap<>();
Thread good = worker("validate", () -> outcomes.put("validate", "ok"), outcomes);
Thread bad =
worker(
"publish",
() -> {
throw new IllegalStateException("offline");
},
outcomes);
good.start();
bad.start();
good.join();
bad.join();
System.out.println("states=" + good.getState() + "," + bad.getState());
System.out.println("outcomes=" + outcomes);
}
private static Thread worker(String name, Runnable task, Map<String, String> outcomes) {
Thread t = new Thread(task, "job-" + name);
t.setUncaughtExceptionHandler(
(thread, error) -> outcomes.put(name, "failed:" + error.getMessage()));
return t;
}
}두 상태는 모두 TERMINATED지만 결과는 다릅니다.
ConcurrentHashMap은 처리기와 작업자가 서로 다른 스레드에서 쓰는 저장소라 선택했습니다.
priority·ThreadGroup의 한계
플랫폼 스레드의 우선순위 범위는 1부터 10이고 NORM_PRIORITY는 5입니다. 생성자의 초기값은 생성 스레드에서 상속하되 스레드 그룹의 최대값을 넘지 않습니다.
값은 스케줄러 힌트이며 높은 우선순위가 먼저 시작하거나 먼저 끝난다는 이식 가능한 보장은 없습니다.
기아를 막는 업무 정책은 공정 큐, 세마포어, 비율 제한기처럼 의미 있는 제어 구조로 표현합니다.
ThreadGroup은 이름 묶음과 일부 일괄 작업을 제공하지만 구조화된 소유권과 결과 수집이 약합니다.
새 코드에서는 스레드 팩터리가 이름·daemon·처리기를 일관되게 설정하고 실행기가 shutdown을 소유하게 합니다.
현재 스레드 그룹을 보안 수단으로 보지 않습니다.
연습 문제
래치를 기다리는 작업자를 만들고 start 전, 대기 중, 게이트 해제와 join 후의 상태를 출력하세요.
sleep만으로 WAITING 진입을 추측하지 말고 폴링에 상한을 두세요.
정답과 관찰 한계
래치 대기는 시간 제한이 없으므로 WAITING입니다.
폴링에는 nanoTime 기준 1초 종료 조건을 둡니다. 루프 뒤 WAITING인지 확인해 실패시키지는 않으므로, 제한에 도달하면 실제로 관측한 다른 상태가 during에 출력될 수 있습니다.
import java.util.concurrent.CountDownLatch;
public final class ThreeLifecycleSnapshotsSolution {
public static void main(String[] args) throws InterruptedException {
CountDownLatch gate = new CountDownLatch(1);
Thread worker =
new Thread(
() -> {
try {
gate.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
},
"snapshot-worker");
System.out.println("before=" + worker.getState());
worker.start();
long deadline = System.nanoTime() + 1_000_000_000L;
while (worker.getState() != Thread.State.WAITING && System.nanoTime() < deadline)
Thread.onSpinWait();
System.out.println("during=" + worker.getState());
gate.countDown();
worker.join();
System.out.println("after=" + worker.getState());
}
}WAITING 진입을 관측했다면 before=NEW, during=WAITING, after=TERMINATED가 출력됩니다. 가운데 상태는 스케줄링에 따라 달라질 수 있고, 마지막 종료는 래치 해제와 join으로 기다립니다.
실제 운영 로직이 상태 폴링에 의존해서는 안 되며 래치 자체가 조정 규칙입니다.
Thread 상태는 원인을 찾는 관찰 vocabulary입니다.
식별 정보와 결과를 별도로 남기고, 검사 예외 구분을 처리하며, 우선순위가 아니라 명시적 동기화로 순서를 정해야 합니다.