본문 바로가기

스레드 정보와 생명 주기

id·이름·우선순위·그룹을 관찰하고 NEW부터 TERMINATED까지 상태 전이를 제어된 사건으로 재현합니다.

Thread 객체는 작업뿐 아니라 식별 정보와 현재 생명 주기 정보를 제공합니다.

이 값은 관찰과 진단에 유용하지만 업무 순서를 보장하는 제어 장치로 쓰면 안 됩니다.

상태 이름이 어떤 대기 원인을 뜻하는지 실제 사건과 연결해야 스레드 덤프도 읽을 수 있습니다.


run 재정의의 검사 예외 제한

Runnable.run()과 Thread.run()은 검사 예외를 선언하지 않습니다.

override 메서드는 부모보다 넓은 검사 예외를 새로 던질 수 없으므로 아래 코드는 실제 컴파일 오류입니다.

lab/CheckedExceptionRunFailure.java
public final class CheckedExceptionRunFailure extends Thread {
    @Override
    public void run() throws InterruptedException {
        Thread.sleep(10);
    }
}

스레드 접점에서는 InterruptedException을 catch해 중단 정책을 수행해야 합니다.

단순히 RuntimeException으로 감싸면 협력적 취소 의미를 잃을 수 있습니다.

복구할 수 없으면 상태를 정리하고 interrupt 플래그를 복원하거나 결과 추상화로 실패를 전달합니다.


스레드 ID·이름과 업무 키

threadId()는 생성할 때 부여되는 양수이며 해당 스레드의 수명 동안 고유하고 바뀌지 않습니다. 재실행 후에도 유지되는 영구 업무 식별자는 아닙니다.

이 문서의 생성자로 만드는 플랫폼 스레드는 이름을 지정하지 않으면 Thread-N 계열 기본 이름을 받습니다.

운영 로그에는 역할이 드러나는 이름을 부여하되 주문 번호 같은 업무 키는 별도 구조화 필드로 남깁니다.

src/ThreadMetadataReport.java
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());
    }
}

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

main-before/NEW/worker inside/join 뒤 TERMINATED의 실제 조회 시점입니다. 고정 조회 대상과 현재 실행 흐름을 단계마다 구분해 조회 대상을 확인할 수 있습니다.

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

ThreadMetadataReport는 report 안에서 현재 스레드를 조회하고, main에서는 작업자의 시작 전 상태와 그룹 및 join 뒤 종료 상태를 조회합니다.

스레드 정보 · 조회 대상

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

report는 호출한 현재 스레드를 조회합니다. worker의 NEW·TERMINATED 상태 조회는 별도의 객체 접근입니다.

호출 지점마다 달라지는 조회 대상원순서의main현재흐름조회,worker.NEW조회,worker안현재흐름조회,join뒤worker조회입니다. currentThread와worker객체접근의 대상을 구별하고 실제ID숫자를 고정하지 않습니다.
main-before 보고currentThread는 main
연결 관계main-before 보고 · currentThread는 main → 시작 전 worker 조회 · NEW / import-workers 그룹
시작 전 worker 조회NEW / import-workers 그룹
연결 관계시작 전 worker 조회 · NEW / import-workers 그룹 → worker의 inside 보고 · 현재 흐름은 import-csv
worker의 inside 보고현재 흐름은 import-csv
연결 관계worker의 inside 보고 · 현재 흐름은 import-csv → worker join 뒤 조회 · TERMINATED / alive=false
worker join 뒤 조회TERMINATED / alive=false

실선: 원 호출·조회 시점ID는 고정 숫자로 외우지 않음우선순위는 완료 순서 계약 아님

호출 지점마다 달라지는 조회 대상원순서의main현재흐름조회,worker.NEW조회,worker안현재흐름조회,join뒤worker조회입니다. currentThread와worker객체접근의 대상을 구별하고 실제ID숫자를 고정하지 않습니다.
main-before 보고currentThread는 main
연결 관계main-before 보고 · currentThread는 main → 시작 전 worker 조회 · NEW / import-workers 그룹
시작 전 worker 조회NEW / import-workers 그룹
연결 관계시작 전 worker 조회 · NEW / import-workers 그룹 → worker의 inside 보고 · 현재 흐름은 import-csv
worker의 inside 보고현재 흐름은 import-csv
연결 관계worker의 inside 보고 · 현재 흐름은 import-csv → worker join 뒤 조회 · TERMINATED / alive=false
worker join 뒤 조회TERMINATED / alive=false

실선: 원 호출·조회 시점ID는 고정 숫자로 외우지 않음우선순위는 완료 순서 계약아님

  1. report("main-before")

    조회 대상
    현재 실행 중인 main
    출력에서 구분할 정보
    id / name / priority / daemon
  2. 시작 전 worker.getState()·그룹 조회

    조회 대상
    worker 객체
    출력에서 구분할 정보
    new-state=NEW, group=import-workers
  3. report("inside")

    조회 대상
    실행 중인 import-csv
    출력에서 구분할 정보
    id / name / priority / daemon
  4. worker.join() 뒤 조회

    조회 대상
    worker 객체
    출력에서 구분할 정보
    end-state=TERMINATED, alive=false

report는 Thread.currentThread()를 읽습니다. 원문은 NEW인 작업자의 id를 main에서 출력하지 않습니다. id와 우선순위는 업무 식별자나 완료 순서의 보장이 아닙니다.

이 예의 조건

  • ID 수치는 고정하지 않고 우선순위를 완료 순서로 읽지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원래 순서의 main 현재 흐름 조회, worker.NEW 조회, worker 안의 현재 흐름 조회, join 뒤 worker 조회입니다. currentThread와 worker 객체 접근의 대상을 구별하고 실제 ID 숫자를 고정하지 않습니다.
6NEW
7import-workers 그룹
11TERMINATED
12alive=false
원 도표의 설명과 주석
구분내용
설명원래 순서의 main 현재 흐름 조회, worker.NEW 조회, worker 안의 현재 흐름 조회, join 뒤 worker 조회입니다. currentThread와 worker 객체 접근의 대상을 구별하고 실제 ID 숫자를 고정하지 않습니다.
전체 읽기 정보같은 main 코드라도 조회 대상은 호출 지점마다 다르다 같은 main 코드라도 조회 대상은 호출 지점마다 다르다 ThreadMetadataReport는 report 안에서 현재 스레드를 조회하고, main에서는 작업자의 시작 전 상태와 그룹 및 join 뒤 종료 상태를 조회합니다. 스레드 정보 · 조회 대상 같은 main 코드라도 조회 대상은 호출 지점마다 다르다 report는 호출한 현재 스레드를 조회합니다. worker의 NEW·TERMINATED 상태 조회는 별도의 객체 접근입니다. 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와 우선순위는 업무 식별자나 완료 순서의 보장이 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
시작 전 조회

main과 worker 객체를 서로 다른 대상으로 조회합니다.

  • currentThread: main
  • worker: NEW / import-workers
  • 현재 흐름 조회와 다른 객체의 메타 조회가 다르다.
worker 내부

작업자 안에서 현재 흐름을 보고합니다.

  • currentThread: import-csv
  • 조회 지점의 실행 주체가 main에서 worker로 바뀐다.
join 뒤 조회

종료된 worker를 main이 읽습니다.

  • state: TERMINATED
  • alive: false
  • 종료 이후 시점의 메타값은 시작 전과 다르다.

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

main-before/NEW/worker inside/join 뒤 TERMINATED의 실제 조회 시점입니다. 고정 조회 대상과 현재 실행 흐름을 단계마다 구분해 조회 대상을 확인할 수 있습니다.

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

ThreadMetadataReport는 report 안에서 현재 스레드를 조회하고, main에서는 작업자의 시작 전 상태와 그룹 및 join 뒤 종료 상태를 조회합니다.

스레드 정보 · 조회 대상

같은 main 코드라도 조회 대상은 호출 지점마다 다르다

report는 호출한 현재 스레드를 조회합니다. worker의 NEW·TERMINATED 상태 조회는 별도의 객체 접근입니다.

호출 지점마다 달라지는 조회 대상원순서의main현재흐름조회,worker.NEW조회,worker안현재흐름조회,join뒤worker조회입니다. currentThread와worker객체접근의 대상을 구별하고 실제ID숫자를 고정하지 않습니다.
main-before 보고currentThread는 main
연결 관계main-before 보고 · currentThread는 main → 시작 전 worker 조회 · NEW / import-workers 그룹
시작 전 worker 조회NEW / import-workers 그룹
연결 관계시작 전 worker 조회 · NEW / import-workers 그룹 → worker의 inside 보고 · 현재 흐름은 import-csv
worker의 inside 보고현재 흐름은 import-csv
연결 관계worker의 inside 보고 · 현재 흐름은 import-csv → worker join 뒤 조회 · TERMINATED / alive=false
worker join 뒤 조회TERMINATED / alive=false

실선: 원 호출·조회 시점ID는 고정 숫자로 외우지 않음우선순위는 완료 순서 계약 아님

호출 지점마다 달라지는 조회 대상원순서의main현재흐름조회,worker.NEW조회,worker안현재흐름조회,join뒤worker조회입니다. currentThread와worker객체접근의 대상을 구별하고 실제ID숫자를 고정하지 않습니다.
main-before 보고currentThread는 main
연결 관계main-before 보고 · currentThread는 main → 시작 전 worker 조회 · NEW / import-workers 그룹
시작 전 worker 조회NEW / import-workers 그룹
연결 관계시작 전 worker 조회 · NEW / import-workers 그룹 → worker의 inside 보고 · 현재 흐름은 import-csv
worker의 inside 보고현재 흐름은 import-csv
연결 관계worker의 inside 보고 · 현재 흐름은 import-csv → worker join 뒤 조회 · TERMINATED / alive=false
worker join 뒤 조회TERMINATED / alive=false

실선: 원 호출·조회 시점ID는 고정 숫자로 외우지 않음우선순위는 완료 순서 계약아님

  1. report("main-before")

    조회 대상
    현재 실행 중인 main
    출력에서 구분할 정보
    id / name / priority / daemon
  2. 시작 전 worker.getState()·그룹 조회

    조회 대상
    worker 객체
    출력에서 구분할 정보
    new-state=NEW, group=import-workers
  3. report("inside")

    조회 대상
    실행 중인 import-csv
    출력에서 구분할 정보
    id / name / priority / daemon
  4. worker.join() 뒤 조회

    조회 대상
    worker 객체
    출력에서 구분할 정보
    end-state=TERMINATED, alive=false

report는 Thread.currentThread()를 읽습니다. 원문은 NEW인 작업자의 id를 main에서 출력하지 않습니다. id와 우선순위는 업무 식별자나 완료 순서의 보장이 아닙니다.

이 예의 조건

  • ID 수치는 고정하지 않고 우선순위를 완료 순서로 읽지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원래 순서의 main 현재 흐름 조회, worker.NEW 조회, worker 안의 현재 흐름 조회, join 뒤 worker 조회입니다. currentThread와 worker 객체 접근의 대상을 구별하고 실제 ID 숫자를 고정하지 않습니다.
6NEW
7import-workers 그룹
11TERMINATED
12alive=false
원 도표의 설명과 주석
구분내용
설명원래 순서의 main 현재 흐름 조회, worker.NEW 조회, worker 안의 현재 흐름 조회, join 뒤 worker 조회입니다. currentThread와 worker 객체 접근의 대상을 구별하고 실제 ID 숫자를 고정하지 않습니다.
전체 읽기 정보같은 main 코드라도 조회 대상은 호출 지점마다 다르다 같은 main 코드라도 조회 대상은 호출 지점마다 다르다 ThreadMetadataReport는 report 안에서 현재 스레드를 조회하고, main에서는 작업자의 시작 전 상태와 그룹 및 join 뒤 종료 상태를 조회합니다. 스레드 정보 · 조회 대상 같은 main 코드라도 조회 대상은 호출 지점마다 다르다 report는 호출한 현재 스레드를 조회합니다. worker의 NEW·TERMINATED 상태 조회는 별도의 객체 접근입니다. 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와 우선순위는 업무 식별자나 완료 순서의 보장이 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
시작 전 조회

main과 worker 객체를 서로 다른 대상으로 조회합니다.

  • currentThread: main
  • worker: NEW / import-workers
  • 현재 흐름 조회와 다른 객체의 메타 조회가 다르다.
worker 내부

작업자 안에서 현재 흐름을 보고합니다.

  • currentThread: import-csv
  • 조회 지점의 실행 주체가 main에서 worker로 바뀐다.
join 뒤 조회

종료된 worker를 main이 읽습니다.

  • state: TERMINATED
  • alive: false
  • 종료 이후 시점의 메타값은 시작 전과 다르다.

조회 대상부터 확인하세요. report("main-before")는 main이 호출하므로 currentThread()도 main을 반환합니다. 다음 줄의 worker.getState()는 아직 시작하지 않은 import-csv 객체를 조회하므로 NEW입니다. 같은 report를 작업자 안에서 호출하는 report("inside")는 import-csv 정보를 출력합니다. 출력에 나온 ID 숫자를 외우기보다 어떤 객체·어떤 실행 지점을 관찰했는지 연결하세요.

API상 NEW인 작업자의 id와 이름도 조회할 수 있습니다.

종료된 Thread의 그룹 참조는 null이 될 수 있으므로 그룹은 시작 전에 확인했습니다.

ThreadGroup은 오래된 그룹화 API이고 새 설계의 자원 관리 수단으로는 실행기와 명시적 소유자를 우선합니다.


스레드 상태와 대기 원인

RUNNABLE은 CPU에서 실제 명령을 수행하는 순간뿐 아니라 실행 가능 큐에서 차례를 기다리는 상태도 포함합니다.

BLOCKED는 synchronized 모니터 진입 대기입니다.

WAITING은 시간 제한 없이 다른 사건을, TIMED_WAITING은 제한 시간과 함께 기다립니다.

한 번 읽은 상태는 다음 순간의 약속이 아닙니다. Thread.State API의 상태는 JVM 관점이며 운영체제의 스레드 상태와 일대일 대응하지 않습니다. getState()를 세 번 호출해도 하나의 원자적 스냅샷이 되지 않습니다. 예를 들어 timed를 확인한 뒤 다른 작업자를 확인하는 사이에 500ms sleep이 끝날 수 있습니다. 작업의 선후 관계는 상태 조회가 아니라 래치·락·join 같은 동기화 계약으로 정합니다.

아래 프로그램은 래치로 WAITING, sleep(500)으로 TIMED_WAITING, main이 가진 모니터로 BLOCKED 관측을 시도합니다.

src/ControlledThreadStates.java
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이 보유한 모니터 진입을 각각 관측하려 합니다. 시간 대기는 출력 전에 끝날 수 있고 관측 실패 시 래치가 해제되지 않습니다.

대기 원인 · 래치 / 시간 / 모니터

세 대기 원인과 상태 관측의 한계를 나눠 본다

같은 대기라도 해제 조건은 다릅니다. 상태 조회 세 번은 하나의 원자적 스냅샷이 아닙니다.

대기를 유지하는 서로 다른 조건 래치·sleep·모니터가 각각대기의원인입니다. 상태조회는각원인과연결되는관측이며세대기의순차이행이나원자적스냅샷을 뜻하지 않습니다. 래치 waitingGate.await countDown 전 조건 유지 WAITING 관측 state-waiting 작업자 시간 sleep(500) 시간 경과 / 중단으로 종료 TIMED_WAITING 관측 출력 전에 끝날 수도 있음 모니터 main이 monitor 보유 worker의 synchronized 진입 BLOCKED 관측 해제 뒤 진입 가능 실선: 원인과 관측의 관계 세 조회는 원자적 스냅샷 아님 원 실패 시 래치 정리가 빠질 수 있음
대기를 유지하는 서로 다른 조건 래치·sleep·모니터가 각각대기의원인입니다. 상태조회는각원인과연결되는관측이며세대기의순차이행이나원자적스냅샷을 뜻하지 않습니다. 래치 waitingGate.await countDown 전 조건 유지 WAITING 관측 state-waiting 작업자 시간 sleep(500) 시간 경과 / 중단으로 종료 TIMED_WAITING 관측 출력 전에 끝날 수도 있음 모니터 main이 monitor 보유 worker의 synchronized 진입 BLOCKED 관측 해제 뒤 진입 가능 실선: 원인과 관측의 관계 세 조회는 원자적 스냅샷 아님 원 실패 시 래치 정리가 빠질 수 있음

WAITING
state-waiting

대기를 만드는 원문
waitingGate.await()
유지 조건과 관측 한계
래치가 0이 될 때까지 기다림; countDown() 전에는 대기 원인이 유지됨

TIMED_WAITING
state-timed

대기를 만드는 원문
Thread.sleep(500)
유지 조건과 관측 한계
시간 경과나 interrupt로 대기에서 벗어남; 폴링 뒤 출력 시점까지 유지된다는 보장은 없음

BLOCKED
state-blocked

대기를 만드는 원문
synchronized (monitor)
유지 조건과 관측 한계
main이 모니터를 가진 동안 진입 대기; 모니터 해제 뒤 진입 가능

waitFor는 목표 상태를 관측했는지 확인합니다. 세 상태 조회는 하나의 원자적 스냅샷이 아니며, 관측 실패로 예외가 나면 래치를 해제하지 않아 비데몬 대기자가 남을 수 있습니다.

상태를 변경하는 원인은 상태 조회가 아니라 사건의 완료입니다.

이 코드에서 main이 잠들어도 모니터는 그대로입니다. waitFor의 Thread.sleep(1)은 이를 호출한 main을 잠깐 쉬게 합니다. Thread.sleep API에 따라 현재 가진 모니터의 소유권은 놓지 않으므로, 바깥 synchronized (monitor)를 빠져나오기 전에는 blocked 작업자가 같은 모니터에 진입할 수 없습니다. sleep 시간을 줄이는 것과 모니터를 해제하는 것은 서로 다른 동작입니다.

원 ControlledThreadStates는 목표 상태를 관측하지 못해 예외가 나면 waitingGate를 해제하는 줄까지 도달하지 못합니다. 그때 비데몬 작업자가 남을 수 있습니다. 관측 시도에 상한을 두는 일과 실패 후 작업자를 정리하는 일은 따로 필요합니다. 또한 main이 monitor를 가진 synchronized 안에서 blocked 작업자를 join하면 그 작업자는 monitor에 들어갈 수 없으므로 종료를 기다릴 수 없습니다. 모니터를 해제한 뒤 회수해야 합니다.

예시

다음 별도 예제는 대기 관측이 성공했는지와 관계없이 finally에서 게이트를 열고 종료를 회수합니다.

src/WaitingObservationCleanup.java
import java.util.concurrent.CountDownLatch;

public final class WaitingObservationCleanup {
    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(); }
        }, "waiting-observation");
        worker.start();
        boolean observed = false;
        try {
            long deadline = System.nanoTime() + 500_000_000L;
            while (System.nanoTime() < deadline) {
                if (worker.getState() == Thread.State.WAITING) {
                    observed = true;
                    break;
                }
                Thread.yield();
            }
            System.out.println("waiting-observed=" + observed);
        } finally {
            gate.countDown();
            worker.join();
        }
        System.out.println("alive-after-cleanup=" + worker.isAlive());
    }
}

첫 관측은 스케줄링에 따라 true 또는 false일 수 있고, 정상 반환한 마지막 join 뒤 생존 여부는 false입니다. yield가 상태 진입을 보장하는 것이 아니라 게이트가 대기 조건을 유지합니다. 정리까지 500ms 안에 끝난다는 실시간 보장도 하지 않습니다.


스레드 생명 주기의 비가역성

start는 NEW인 스레드의 실행을 요청합니다. 실행 중 여러 대기 상태를 거칠 수 있지만, 호출 직후 getState()로 RUNNABLE이 반드시 관측되는 것은 아닙니다.

run()이 정상 반환하거나, 처리되지 않은 예외 뒤 예외 처리기까지 끝나면 TERMINATED입니다.

TERMINATED 객체를 다시 start할 수 없습니다.

isAlive()는 start 이후 종료 전까지 true지만 작업이 유용하게 진행 중이라는 뜻은 아닙니다.

락 교착 상태나 무한 WAITING도 생존 상태입니다.

상태 확인에는 큐 진척, 마지막 완료 시각, 시간 제한 같은 애플리케이션 신호를 함께 사용합니다.

처리되지 않은 예외로 끝난 스레드도 TERMINATED입니다.

getState()만 보면 성공·실패를 구별할 수 없으므로 처리기나 Future 결과로 원인을 기록합니다.

app/LifecycleOutcomeRegistry.java
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지만 결과는 다릅니다.

같은 상태 이름이 나오는 이유는 getState가 run의 생명 주기를 설명하기 때문입니다. 성공 작업은 정상 반환하고 실패 작업은 예외 처리기까지 마친 뒤 종료합니다. 운영 판정에서 state 한 칸만 저장하면 실패 원인이 없어지므로, 작업 식별자와 성공·실패 기록을 함께 읽어야 합니다. 상태 조회가 한 번 더 필요해서가 아니라 관측하려는 사실 자체가 다릅니다.

ConcurrentHashMap은 처리기와 작업자가 서로 다른 스레드에서 쓰는 저장소라 선택했습니다.


priority·ThreadGroup의 한계

플랫폼 스레드의 우선순위 범위는 1부터 10이고 NORM_PRIORITY는 5입니다. 생성자의 초깃값은 생성 스레드에서 상속하되 스레드 그룹의 최댓값을 넘지 않습니다.

값은 스케줄러 힌트이며 높은 우선순위가 먼저 시작하거나 먼저 끝난다는 이식 가능한 보장은 없습니다.

기아를 막는 업무 정책은 공정 큐, 세마포어, 비율 제한기처럼 의미 있는 제어 구조로 표현합니다.

ThreadGroup은 이름 묶음과 일부 일괄 작업을 제공하지만 구조화된 소유권과 결과 수집이 약합니다.

새 코드에서는 스레드 팩터리가 이름·daemon·처리기를 일관되게 설정하고 실행기가 shutdown을 소유하게 합니다.

현재 스레드 그룹을 보안 수단으로 보지 않습니다.


연습 문제

래치를 기다리는 작업자를 만들고 start 전, 대기 중, 게이트 해제와 join 후의 상태를 출력하세요.

sleep만으로 WAITING 진입을 추측하지 말고 폴링에 상한을 두세요.

정답과 관찰 한계

래치 대기는 시간 제한이 없으므로 WAITING입니다.

폴링에는 nanoTime 기준 1초 종료 조건을 둡니다. 루프 뒤 WAITING인지 확인해 실패시키지는 않으므로, 제한에 도달하면 실제로 관측한 다른 상태가 during에 출력될 수 있습니다.

exercise/ThreeLifecycleSnapshotsSolution.java
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 상태는 원인을 찾는 데 사용하는 관찰용 용어입니다.

식별 정보와 결과를 별도로 남기고, 검사 예외를 처리하며, 우선순위가 아니라 명시적 동기화로 순서를 정해야 합니다.


조회 대상·대기 원인·업무 결과 구분하기

다음 세 상황은 서로 독립적입니다. 스레드는 이 문서의 생성자로 만든 플랫폼 스레드이며, 별도 interrupt나 외부 종료는 없다고 가정합니다.

  1. main이 Thread worker = new Thread(() -> report("inside"), "import-csv")를 만든 뒤, start 전에 report("main-before")와 worker.getState()를 차례로 실행합니다. 두 조회는 누구를 대상으로 하며 worker 상태는 무엇인가요? start 이후 inside의 currentThread()는 누구인가요?
  2. main은 synchronized (monitor) 안에 있고, 작업자는 같은 monitor에 진입하려고 기다립니다. main이 Thread.sleep(10)을 호출하면 잠드는 대상은 누구이며 모니터가 작업자에게 넘어가나요? 이 구역 안에서 작업자를 join하면 왜 종료를 기다릴 수 없나요?
  3. WAITING, TIMED_WAITING, BLOCKED를 서로 다른 작업자에게서 차례로 읽었습니다. 세 값이 같은 순간의 상태라고 결론 낼 수 있나요? 나중에 두 작업자가 모두 TERMINATED가 되었는데 하나는 정상 완료, 다른 하나는 예외 종료였습니다. 상태만으로 이를 구분할 수 있나요?
상태 관찰의 해설
  1. report("main-before") 안의 currentThread()는 호출자인 main입니다. worker.getState()의 수신 객체는 아직 start하지 않은 import-csv이므로 NEW입니다. inside는 작업자에서 실행하므로 import-csv를 반환합니다. 이름이 붙은 객체를 만들었다는 사실만으로 현재 실행 중인 스레드가 바뀌지는 않습니다.
  2. sleep을 호출한 main이 잠들며 모니터를 계속 소유합니다. 작업자는 여전히 진입할 수 없습니다. 구역 안의 join은 모니터를 넘기지 않은 채, 그 모니터를 얻어야 끝낼 수 있는 작업자의 종료를 기다리는 순환 대기입니다. 먼저 synchronized 구역을 벗어나고 나서 join해야 합니다.
  3. 서로 다른 시점의 조회라 하나의 순간을 보장하지 않습니다. TERMINATED도 종료 사실만 알려 주므로 성공·실패를 구분하지 못합니다. 순서는 래치·락·join으로 제어하고, 성공·실패는 이 문서의 outcomes 같은 별도 결과 기록으로 확인해야 합니다.