본문 바로가기

정상 종료와 yield

플래그만 바꾸는 종료 실패를 고치고 interrupt·큐·join을 결합하며 sleep·yield·빈 상태 동작을 비교합니다.

백그라운드 작업자 종료는 요청, 대기 해제, 진행 중 작업 처리, 최종 회수의 네 단계로 봐야 합니다.

volatile 플래그만 바꾸면 큐에서 무기한 기다리는 스레드를 깨우지 못하고, interrupt만 보내면 남은 항목 처리 정책이 없습니다.

printer 사례로 두 신호를 하나의 생명 주기로 묶습니다.


running 플래그와 take 대기

아래 daemon 작업자는 빈 큐의 take()에서 WAITING입니다.

main이 volatile 플래그를 false로 써도 대기가 풀리지 않아 실제로 wrong-alive-after-flag=true가 출력됩니다.

마지막 interrupt는 예제 프로세스가 스레드를 남기지 않기 위한 정리입니다.

lab/FlagOnlyShutdownBug.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class FlagOnlyShutdownBug {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        Control control = new Control();
        Thread worker =
                new Thread(
                        () -> {
                            try {
                                while (control.running) {
                                    System.out.println(queue.take());
                                }
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        },
                        "flag-only-worker");
        worker.setDaemon(true);
        worker.start();
        while (worker.getState() != Thread.State.WAITING) {
            Thread.onSpinWait();
        }
        control.running = false;
        worker.join(20);
        System.out.println("wrong-alive-after-flag=" + worker.isAlive());
        worker.interrupt();
        worker.join();
    }

    private static final class Control {
        volatile boolean running = true;
    }
}

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

빈 take 대기→volatile flag만 변경→join(20) 후 alive→interrupt→join의 실제 관측 순서입니다. 가시성과 wake-up의 차이를 상태로 추적하기 좋습니다.

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

FlagOnlyShutdownBug에서 대기 진입, volatile 플래그 변경, interrupt와 join 정리의 역할을 나눕니다.

SHUTDOWN · 가시성과 깨우기

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

volatile 쓰기는 값의 가시성을 제공합니다. take 대기를 깨우려면 별도의 중단 신호가 필요합니다.

가시성 변경과 대기를 깨우는 신호빈큐take대기가volatile running=false로풀리지않아join20뒤에도생존합니다. interrupt대기해제뒤join종료회수의원순서를연결합니다.
빈 큐의 takeWAITING 진입 관측
연결 관계빈 큐의 take · WAITING 진입 관측 → main이 running=false · volatile값 가시성만 변경
main이 running=falsevolatile값 가시성만 변경
연결 관계main이 running=false · volatile값 가시성만 변경 → main의 join(20) 반환 · wrong-alive-after- · flag=true
main의 join(20) 반환wrong-alive-after-flag=true
연결 관계main의 join(20) 반환 · wrong-alive-after- · flag=true → main이 interrupt 요청 · take 대기가 예외로 해제
main이 interrupt 요청take 대기가 예외로 해제
연결 관계main이 interrupt 요청 · take 대기가 예외로 해제 → main의 join 정상 반환 · worker 종료 회수
main의 join 정상 반환worker 종료 회수

실선: 원 main의 관측·요청 순서플래그 쓰기는 대기를 깨우지 않음원 폴링은 시간 상한이 없음

가시성 변경과 대기를 깨우는 신호빈큐take대기가volatile running=false로풀리지않아join20뒤에도생존합니다. interrupt대기해제뒤join종료회수의원순서를연결합니다.
빈 큐의 takeWAITING 진입 관측
연결 관계빈 큐의 take · WAITING 진입 관측 → main이 running=false · volatile값 가시성만 변경
main이 running=falsevolatile값 가시성만 변경
연결 관계main이 running=false · volatile값 가시성만 변경 → main의 join(20) 반환 · wrong-alive-after- · flag=true
main의 join(20) 반환wrong-alive-after-flag=true
연결 관계main의 join(20) 반환 · wrong-alive-after- · flag=true → main이 interrupt 요청 · take 대기가 예외로 해제
main이 interrupt 요청take 대기가 예외로 해제
연결 관계main이 interrupt 요청 · take 대기가 예외로 해제 → main의 join 정상 반환 · worker 종료 회수
main의 join 정상 반환worker 종료 회수

실선: 원 main의 관측·요청순서플래그 쓰기는 대기를 깨우지않음원 폴링은 시간 상한이 없음

  1. 빈 큐의 take()

    작업자 조건
    WAITING 관찰 뒤 다음 단계로 진행
    확인 범위
    항목이나 중단 신호를 기다림
  2. running=false 뒤 join(20)

    작업자 조건
    플래그 쓰기가 take를 깨우지 않음
    확인 범위
    wrong-alive-after-flag=true
  3. interrupt() 뒤 join()

    작업자 조건
    대기를 빠져나와 작업자가 종료
    확인 범위
    정리 완료 · 별도 완료 로그는 없음

상태 확인 반복에는 자체 상한이 없습니다. daemon 설정이 이 종료 절차를 대신하지 않으며, 표의 마지막 행은 원문 제어 흐름을 설명합니다.

이 예의 조건

  • WAITING 진입을 관측하는 원 폴링에는 시간 상한이 없다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2빈 큐의 take 대기가 volatile running=false로 풀리지 않아 join(20) 뒤에도 생존합니다. interrupt로 대기를 해제한 뒤 join으로 종료를 회수하는 원래 순서를 연결합니다.
6volatile 값 가시성만 변경
원 도표의 설명과 주석
구분내용
설명빈 큐의 take 대기가 volatile running=false로 풀리지 않아 join(20) 뒤에도 생존합니다. interrupt로 대기를 해제한 뒤 join으로 종료를 회수하는 원래 순서를 연결합니다.
전체 읽기 정보running을 false로 써도 빈 큐의 대기는 풀리지 않는다 running을 false로 써도 빈 큐의 대기는 풀리지 않는다 FlagOnlyShutdownBug에서 대기 진입, volatile 플래그 변경, interrupt와 join 정리의 역할을 나눕니다. SHUTDOWN · 가시성과 깨우기 running을 false로 써도 빈 큐의 대기는 풀리지 않는다 volatile 쓰기는 값의 가시성을 제공합니다. take 대기를 깨우려면 별도의 중단 신호가 필요합니다. 빈 큐의 take() 작업자 조건 WAITING 관찰 뒤 다음 단계로 진행 확인 범위 항목이나 중단 신호를 기다림 running=false 뒤 join(20) 작업자 조건 플래그 쓰기가 take를 깨우지 않음 확인 범위 wrong-alive-after-flag=true interrupt() 뒤 join() 작업자 조건 대기를 빠져나와 작업자가 종료 확인 범위 정리 완료 · 별도 완료 로그는 없음 상태 확인 반복에는 자체 상한이 없습니다. daemon 설정이 이 종료 절차를 대신하지 않으며, 표의 마지막 행은 원문 제어 흐름을 설명합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
take 대기

worker가 빈 큐에서 기다립니다.

  • 상태 관측: WAITING
  • running: true
  • 조건값과 블로킹 대기는 다른 상태다.
플래그만 변경

main이 running=false 후 timed join합니다.

  • running: false
  • join(20) 뒤: wrong-alive-after-flag=true
  • volatile 가시성이 대기 자체를 깨우지 않는다.
interrupt 회수

main이 interrupt하고 무기한 join합니다.

  • take: 예외로 해제
  • join: 정상 반환
  • worker: 종료
  • 별도 깨우기 신호와 완료 회수가 필요하다.

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

빈 take 대기→volatile flag만 변경→join(20) 후 alive→interrupt→join의 실제 관측 순서입니다. 가시성과 wake-up의 차이를 상태로 추적하기 좋습니다.

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

FlagOnlyShutdownBug에서 대기 진입, volatile 플래그 변경, interrupt와 join 정리의 역할을 나눕니다.

SHUTDOWN · 가시성과 깨우기

running을 false로 써도 빈 큐의 대기는 풀리지 않는다

volatile 쓰기는 값의 가시성을 제공합니다. take 대기를 깨우려면 별도의 중단 신호가 필요합니다.

가시성 변경과 대기를 깨우는 신호빈큐take대기가volatile running=false로풀리지않아join20뒤에도생존합니다. interrupt대기해제뒤join종료회수의원순서를연결합니다.
빈 큐의 takeWAITING 진입 관측
연결 관계빈 큐의 take · WAITING 진입 관측 → main이 running=false · volatile값 가시성만 변경
main이 running=falsevolatile값 가시성만 변경
연결 관계main이 running=false · volatile값 가시성만 변경 → main의 join(20) 반환 · wrong-alive-after- · flag=true
main의 join(20) 반환wrong-alive-after-flag=true
연결 관계main의 join(20) 반환 · wrong-alive-after- · flag=true → main이 interrupt 요청 · take 대기가 예외로 해제
main이 interrupt 요청take 대기가 예외로 해제
연결 관계main이 interrupt 요청 · take 대기가 예외로 해제 → main의 join 정상 반환 · worker 종료 회수
main의 join 정상 반환worker 종료 회수

실선: 원 main의 관측·요청 순서플래그 쓰기는 대기를 깨우지 않음원 폴링은 시간 상한이 없음

가시성 변경과 대기를 깨우는 신호빈큐take대기가volatile running=false로풀리지않아join20뒤에도생존합니다. interrupt대기해제뒤join종료회수의원순서를연결합니다.
빈 큐의 takeWAITING 진입 관측
연결 관계빈 큐의 take · WAITING 진입 관측 → main이 running=false · volatile값 가시성만 변경
main이 running=falsevolatile값 가시성만 변경
연결 관계main이 running=false · volatile값 가시성만 변경 → main의 join(20) 반환 · wrong-alive-after- · flag=true
main의 join(20) 반환wrong-alive-after-flag=true
연결 관계main의 join(20) 반환 · wrong-alive-after- · flag=true → main이 interrupt 요청 · take 대기가 예외로 해제
main이 interrupt 요청take 대기가 예외로 해제
연결 관계main이 interrupt 요청 · take 대기가 예외로 해제 → main의 join 정상 반환 · worker 종료 회수
main의 join 정상 반환worker 종료 회수

실선: 원 main의 관측·요청순서플래그 쓰기는 대기를 깨우지않음원 폴링은 시간 상한이 없음

  1. 빈 큐의 take()

    작업자 조건
    WAITING 관찰 뒤 다음 단계로 진행
    확인 범위
    항목이나 중단 신호를 기다림
  2. running=false 뒤 join(20)

    작업자 조건
    플래그 쓰기가 take를 깨우지 않음
    확인 범위
    wrong-alive-after-flag=true
  3. interrupt() 뒤 join()

    작업자 조건
    대기를 빠져나와 작업자가 종료
    확인 범위
    정리 완료 · 별도 완료 로그는 없음

상태 확인 반복에는 자체 상한이 없습니다. daemon 설정이 이 종료 절차를 대신하지 않으며, 표의 마지막 행은 원문 제어 흐름을 설명합니다.

이 예의 조건

  • WAITING 진입을 관측하는 원 폴링에는 시간 상한이 없다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2빈 큐의 take 대기가 volatile running=false로 풀리지 않아 join(20) 뒤에도 생존합니다. interrupt로 대기를 해제한 뒤 join으로 종료를 회수하는 원래 순서를 연결합니다.
6volatile 값 가시성만 변경
원 도표의 설명과 주석
구분내용
설명빈 큐의 take 대기가 volatile running=false로 풀리지 않아 join(20) 뒤에도 생존합니다. interrupt로 대기를 해제한 뒤 join으로 종료를 회수하는 원래 순서를 연결합니다.
전체 읽기 정보running을 false로 써도 빈 큐의 대기는 풀리지 않는다 running을 false로 써도 빈 큐의 대기는 풀리지 않는다 FlagOnlyShutdownBug에서 대기 진입, volatile 플래그 변경, interrupt와 join 정리의 역할을 나눕니다. SHUTDOWN · 가시성과 깨우기 running을 false로 써도 빈 큐의 대기는 풀리지 않는다 volatile 쓰기는 값의 가시성을 제공합니다. take 대기를 깨우려면 별도의 중단 신호가 필요합니다. 빈 큐의 take() 작업자 조건 WAITING 관찰 뒤 다음 단계로 진행 확인 범위 항목이나 중단 신호를 기다림 running=false 뒤 join(20) 작업자 조건 플래그 쓰기가 take를 깨우지 않음 확인 범위 wrong-alive-after-flag=true interrupt() 뒤 join() 작업자 조건 대기를 빠져나와 작업자가 종료 확인 범위 정리 완료 · 별도 완료 로그는 없음 상태 확인 반복에는 자체 상한이 없습니다. daemon 설정이 이 종료 절차를 대신하지 않으며, 표의 마지막 행은 원문 제어 흐름을 설명합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
take 대기

worker가 빈 큐에서 기다립니다.

  • 상태 관측: WAITING
  • running: true
  • 조건값과 블로킹 대기는 다른 상태다.
플래그만 변경

main이 running=false 후 timed join합니다.

  • running: false
  • join(20) 뒤: wrong-alive-after-flag=true
  • volatile 가시성이 대기 자체를 깨우지 않는다.
interrupt 회수

main이 interrupt하고 무기한 join합니다.

  • take: 예외로 해제
  • join: 정상 반환
  • worker: 종료
  • 별도 깨우기 신호와 완료 회수가 필요하다.

가시성은 해결됐지만 wake-up이 없습니다.

반대로 interrupt만 써도 현재 항목을 버릴지 완료할지 모호합니다.

종료 API가 큐의 입력 접수 상태와 작업자 상태를 함께 바꿔야 합니다.


프린터 종료 방식

즉시 취소는 현재 출력을 중단하고 큐를 보존·폐기합니다.

비우기 shutdown은 새 요청을 거부하고 이미 받은 항목을 모두 처리한 뒤 끝납니다.

기한을 둔 shutdown은 일정 시간 큐를 비우도록 기다린 뒤 interrupt로 전환합니다.

요구가 다르므로 메서드 이름과 결과도 달라야 합니다.

다음 구현은 poison 명령을 큐에 넣어 비우기 shutdown을 수행합니다. 문서 출력은 실제 장치 작업 대신 콘솔 로그로 나타냅니다.

큐는 FIFO이므로 종료 표식 앞의 문서가 먼저 처리됩니다.

stop 이후 submit을 거부하고 join으로 작업자 종료를 기다립니다.

src/DrainingPrinter.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class DrainingPrinter {
    private static final String STOP = new String("STOP");
    private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    private boolean accepting = true;
    private final Thread worker = new Thread(this::loop, "draining-printer");

    public void start() {
        worker.start();
    }

    public synchronized void submit(String document) {
        if (!accepting) throw new IllegalStateException("printer stopping");
        queue.add(document);
    }

    public void shutdown() throws InterruptedException {
        synchronized (this) {
            if (accepting) {
                accepting = false;
                queue.add(STOP);
            }
        }
        worker.join();
    }

    private void loop() {
        try {
            for (; ; ) {
                String job = queue.take();
                if (job == STOP) break;
                System.out.println("printed=" + job);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("printer interrupted, pending=" + queue.size());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DrainingPrinter p = new DrainingPrinter();
        p.start();
        p.submit("java");
        p.submit("thread");
        p.shutdown();
        System.out.println("printer-stopped");
    }
}

marker는 외부 입력과 식별 정보가 겹치지 않도록 private 객체가 더 적합하지만 큐 타입을 간단히 보이려고 고유 String 인스턴스를 사용했습니다.

제품 구현에서는 sealed 명령이나 별도 closeable 큐 추상화로 표현합니다.

작업자가 예외로 먼저 종료될 때 shutdown join이 돌아오더라도 남은 항목 처리 실패를 결과로 보고해야 합니다.

이 예제의 생명 주기 전제는 start를 한 번 호출한 뒤 submit·shutdown하는 것입니다. 아직 시작하지 않은 스레드에 join하면 바로 돌아오므로, start 전에 shutdown을 호출했다는 사실만으로 큐를 비웠다고 판단할 수 없습니다. 한 번 시작한 Thread는 종료 후 다시 start할 수도 없습니다. 호출 순서를 API로 강제하려면 시작 여부와 접수 여부를 함께 관리해야 합니다.

원 submit의 synchronized 메서드와 shutdown의 synchronized(this)는 같은 객체의 모니터를 사용합니다. 받아들일지 검사하는 일과 문서를 큐에 넣는 일이 한 경계 안에 있어, shutdown이 accepting=false와 STOP을 넣는 사이에 허용된 문서가 뒤늦게 STOP 뒤로 들어가는 틈을 막습니다. 이 두 동작을 락 밖으로 따로 빼면 accepting 값의 가시성만 해결해도 수용 순서는 보장되지 않습니다. join은 모니터 경계를 벗어난 뒤 호출하므로 큐 정책 결정과 종료 대기 역시 구별됩니다.

예시

앞의 src/DrainingPrinter.java와 같은 기본 패키지에 다음 파일을 두고 두 파일을 함께 컴파일합니다.

src/StopMarkerPayload.java
public final class StopMarkerPayload {
    public static void main(String[] args) throws InterruptedException {
        DrainingPrinter printer = new DrainingPrinter();
        printer.start();
        printer.submit("STOP");
        printer.shutdown();
        System.out.println("payload-complete");
    }
}

출력은 printed=STOP 뒤 payload-complete입니다. 외부 문자열의 내용은 종료 표식과 같지만 원 표식은 private new String 인스턴스이고 원 비교는 ==입니다. 문서 문자열은 같은 참조가 아니므로 처리하고, shutdown이 넣은 고유 표식에서 끝납니다. equals로 바꾸면 같은 내용의 일반 입력도 종료 신호로 오해합니다. 이 실험은 고유 표식의 이유를 보여 주며, 제품에서는 앞의 sealed 명령처럼 입력과 제어 신호의 타입을 구분하는 편이 명확합니다.


즉시 종료의 catch 지점

즉시 취소에서는 interrupt()가 take·sleep을 깨우고 반복문이 반환합니다.

진행 중인 printer 드라이버가 중단 가능하지 않다면 취소가 지연될 수 있습니다.

장치 API의 시간 제한과 초기화 기능을 확인해야 합니다.

src/ImmediatePrinterStop.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class ImmediatePrinterStop {
    private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    private final Thread worker = new Thread(this::run, "immediate-printer");

    void start() {
        worker.start();
    }

    void submit(String value) {
        queue.add(value);
    }

    int stopNow() throws InterruptedException {
        worker.interrupt();
        worker.join();
        return queue.size();
    }

    private void run() {
        try {
            while (true) {
                String value = queue.take();
                Thread.sleep(5);
                System.out.println("printed=" + value);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ImmediatePrinterStop p = new ImmediatePrinterStop();
        p.start();
        p.submit("one");
        p.submit("two");
        int pending = p.stopNow();
        System.out.println("pending-after-stop=" + pending);
    }
}

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

수용 차단·STOP drain과 interrupt 즉시 취소의 독립 종료 과정입니다. 수용/처리중/종료 회수 장면으로 완료 보장의 차이를 보여 줍니다.

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

실제 DrainingPrinter와 ImmediatePrinterStop의 수용 차단, 종료 신호, 콘솔 로그, 회수와 잔여 큐 개수를 비교합니다.

SHUTDOWN · 비우기 / 즉시 취소

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

같은 join 회수라도, STOP은 앞선 문서를 비우고 interrupt는 처리 중 항목을 중단할 수 있습니다.

FIFO 비우기와 처리 중 취소의 다른 계약 Draining은수용차단/FIFO앞문서/STOP/회수,Immediate는interrupt/처리중누락가능/회수입니다. 큐pending0·1·2는미완료전체수가아닙니다. 비우기 종료 새 submit 수용 차단 accepting=false FIFO 문서 뒤 고유 STOP java → thread → STOP 정상 join 뒤 큐 빈 상태 앞 문서 두 로그 완료 즉시 취소 worker에 interrupt 새 submit차단검사 없음 꺼낸 값은 처리 중 pending은 큐의0·1·2 처리 중 값은 큐 밖 join으로 종료 회수 모든 문서완료 보장 안 함 실선: 각 정책의 수용·처리·회수 두 영역의 입력 문자열도 다름 큐 크기에 처리 중 값은 미포함
FIFO 비우기와 처리 중 취소의 다른 계약 Draining은수용차단/FIFO앞문서/STOP/회수,Immediate는interrupt/처리중누락가능/회수입니다. 큐pending0·1·2는미완료전체수가아닙니다. 비우기 종료 새 submit 수용 차단 accepting=false FIFO 문서 뒤 고유 STOP java → thread → STOP 정상 join 뒤 큐 빈 상태 앞 문서 두 로그 완료 즉시 취소 worker에 interrupt 새 submit차단검사 없음 꺼낸 값은 처리 중 pending은 큐의0·1·2 처리 중 값은 큐 밖 join으로 종료 회수 모든 문서완료 보장 안 함 실선: 각 정책의 수용·처리·회수 두 영역의 입력 문자열도 다름 큐 크기에 처리 중 값은 미포함

DrainingPrinter

새 입력
shutdown에서 accepting=false · 이후 submit 거부
종료 신호
앞선 문서 뒤에 고유 STOP을 넣음
이미 받은 값
정상 경로에서 java·thread 순서로 로그
호출자 회수
무기한 join()으로 종료를 기다림
큐의 잔여 항목
정상 비우기 뒤 큐가 빔

ImmediatePrinterStop

새 입력
submit에 수용 차단 검사 없음
종료 신호
작업자에 interrupt() 요청
이미 받은 값
one·two의 앞부분만 로그에 남을 수 있음
호출자 회수
무기한 join()으로 종료를 기다림
큐의 잔여 항목
0·1·2 가능 · 꺼낸 뒤 중단된 값은 제외

두 예제의 출력은 실제 장치 작업이 아닌 콘솔 로그입니다. 즉시 취소의 pending은 모든 미완료 작업 수가 아닙니다. 이미 take한 항목이 sleep 중 중단되면 로그와 큐 개수 모두에 남지 않을 수 있습니다.

이 예의 조건

  • 큐 크기에 처리 중 항목은 포함되지 않는다.
  • 두 종료 예제의 입력 문자열도 다르다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2Draining은 수용 차단/FIFO의 앞 문서/STOP/회수, Immediate는 interrupt/처리 중 누락 가능/회수입니다. 큐 pending의 0·1·2는 미완료 전체 수가 아닙니다.
12새 submit 차단 검사 없음
14pending은 큐의 0·1·2
17모든 문서 완료 보장 안 함
원 도표의 설명과 주석
구분내용
설명Draining은 수용 차단/FIFO의 앞 문서/STOP/회수, Immediate는 interrupt/처리 중 누락 가능/회수입니다. 큐 pending의 0·1·2는 미완료 전체 수가 아닙니다.
전체 읽기 정보비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 실제 DrainingPrinter와 ImmediatePrinterStop의 수용 차단, 종료 신호, 콘솔 로그, 회수와 잔여 큐 개수를 비교합니다. SHUTDOWN · 비우기 / 즉시 취소 비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 같은 join 회수라도, STOP은 앞선 문서를 비우고 interrupt는 처리 중 항목을 중단할 수 있습니다. DrainingPrinter 새 입력 shutdown에서 accepting=false · 이후 submit 거부 종료 신호 앞선 문서 뒤에 고유 STOP 을 넣음 이미 받은 값 정상 경로에서 java·thread 순서로 로그 호출자 회수 무기한 join() 으로 종료를 기다림 큐의 잔여 항목 정상 비우기 뒤 큐가 빔 ImmediatePrinterStop 새 입력 submit에 수용 차단 검사 없음 종료 신호 작업자에 interrupt() 요청 이미 받은 값 one·two의 앞부분만 로그에 남을 수 있음 호출자 회수 무기한 join() 으로 종료를 기다림 큐의 잔여 항목 0·1·2 가능 · 꺼낸 뒤 중단된 값은 제외 두 예제의 출력은 실제 장치 작업이 아닌 콘솔 로그입니다. 즉시 취소의 pending은 모든 미완료 작업 수가 아닙니다. 이미 take한 항목이 sleep 중 중단되면 로그와 큐 개수 모두에 남지 않을 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
정책 시작

비우기는 accepting=false, 즉시는 interrupt를 요청합니다.

  • 비우기: 새 submit 차단
  • 즉시: 새 submit 차단 검사 없음
  • 수용 차단과 현재 작업 취소는 다른 책임이다.
작업 처리

비우기는 java/thread 뒤 STOP, 즉시는 처리 중 누락 가능성이 있습니다.

  • 비우기: 두 문서 로그
  • 즉시 pending: 0·1·2 가능 / 처리 중 제외
  • 대기 큐와 실행 중 작업의 소유를 구분한다.
종료 회수

각 worker를 join합니다.

  • 비우기: 앞 문서 완료, 큐 비움
  • 즉시: 모든 문서 완료 보장 없음
  • 스레드 종료가 업무 전체 성공을 뜻하지 않는다.

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

수용 차단·STOP drain과 interrupt 즉시 취소의 독립 종료 과정입니다. 수용/처리중/종료 회수 장면으로 완료 보장의 차이를 보여 줍니다.

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

실제 DrainingPrinter와 ImmediatePrinterStop의 수용 차단, 종료 신호, 콘솔 로그, 회수와 잔여 큐 개수를 비교합니다.

SHUTDOWN · 비우기 / 즉시 취소

비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다

같은 join 회수라도, STOP은 앞선 문서를 비우고 interrupt는 처리 중 항목을 중단할 수 있습니다.

FIFO 비우기와 처리 중 취소의 다른 계약 Draining은수용차단/FIFO앞문서/STOP/회수,Immediate는interrupt/처리중누락가능/회수입니다. 큐pending0·1·2는미완료전체수가아닙니다. 비우기 종료 새 submit 수용 차단 accepting=false FIFO 문서 뒤 고유 STOP java → thread → STOP 정상 join 뒤 큐 빈 상태 앞 문서 두 로그 완료 즉시 취소 worker에 interrupt 새 submit차단검사 없음 꺼낸 값은 처리 중 pending은 큐의0·1·2 처리 중 값은 큐 밖 join으로 종료 회수 모든 문서완료 보장 안 함 실선: 각 정책의 수용·처리·회수 두 영역의 입력 문자열도 다름 큐 크기에 처리 중 값은 미포함
FIFO 비우기와 처리 중 취소의 다른 계약 Draining은수용차단/FIFO앞문서/STOP/회수,Immediate는interrupt/처리중누락가능/회수입니다. 큐pending0·1·2는미완료전체수가아닙니다. 비우기 종료 새 submit 수용 차단 accepting=false FIFO 문서 뒤 고유 STOP java → thread → STOP 정상 join 뒤 큐 빈 상태 앞 문서 두 로그 완료 즉시 취소 worker에 interrupt 새 submit차단검사 없음 꺼낸 값은 처리 중 pending은 큐의0·1·2 처리 중 값은 큐 밖 join으로 종료 회수 모든 문서완료 보장 안 함 실선: 각 정책의 수용·처리·회수 두 영역의 입력 문자열도 다름 큐 크기에 처리 중 값은 미포함

DrainingPrinter

새 입력
shutdown에서 accepting=false · 이후 submit 거부
종료 신호
앞선 문서 뒤에 고유 STOP을 넣음
이미 받은 값
정상 경로에서 java·thread 순서로 로그
호출자 회수
무기한 join()으로 종료를 기다림
큐의 잔여 항목
정상 비우기 뒤 큐가 빔

ImmediatePrinterStop

새 입력
submit에 수용 차단 검사 없음
종료 신호
작업자에 interrupt() 요청
이미 받은 값
one·two의 앞부분만 로그에 남을 수 있음
호출자 회수
무기한 join()으로 종료를 기다림
큐의 잔여 항목
0·1·2 가능 · 꺼낸 뒤 중단된 값은 제외

두 예제의 출력은 실제 장치 작업이 아닌 콘솔 로그입니다. 즉시 취소의 pending은 모든 미완료 작업 수가 아닙니다. 이미 take한 항목이 sleep 중 중단되면 로그와 큐 개수 모두에 남지 않을 수 있습니다.

이 예의 조건

  • 큐 크기에 처리 중 항목은 포함되지 않는다.
  • 두 종료 예제의 입력 문자열도 다르다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2Draining은 수용 차단/FIFO의 앞 문서/STOP/회수, Immediate는 interrupt/처리 중 누락 가능/회수입니다. 큐 pending의 0·1·2는 미완료 전체 수가 아닙니다.
12새 submit 차단 검사 없음
14pending은 큐의 0·1·2
17모든 문서 완료 보장 안 함
원 도표의 설명과 주석
구분내용
설명Draining은 수용 차단/FIFO의 앞 문서/STOP/회수, Immediate는 interrupt/처리 중 누락 가능/회수입니다. 큐 pending의 0·1·2는 미완료 전체 수가 아닙니다.
전체 읽기 정보비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 실제 DrainingPrinter와 ImmediatePrinterStop의 수용 차단, 종료 신호, 콘솔 로그, 회수와 잔여 큐 개수를 비교합니다. SHUTDOWN · 비우기 / 즉시 취소 비우기 종료와 즉시 취소는 같은 작업을 보장하지 않는다 같은 join 회수라도, STOP은 앞선 문서를 비우고 interrupt는 처리 중 항목을 중단할 수 있습니다. DrainingPrinter 새 입력 shutdown에서 accepting=false · 이후 submit 거부 종료 신호 앞선 문서 뒤에 고유 STOP 을 넣음 이미 받은 값 정상 경로에서 java·thread 순서로 로그 호출자 회수 무기한 join() 으로 종료를 기다림 큐의 잔여 항목 정상 비우기 뒤 큐가 빔 ImmediatePrinterStop 새 입력 submit에 수용 차단 검사 없음 종료 신호 작업자에 interrupt() 요청 이미 받은 값 one·two의 앞부분만 로그에 남을 수 있음 호출자 회수 무기한 join() 으로 종료를 기다림 큐의 잔여 항목 0·1·2 가능 · 꺼낸 뒤 중단된 값은 제외 두 예제의 출력은 실제 장치 작업이 아닌 콘솔 로그입니다. 즉시 취소의 pending은 모든 미완료 작업 수가 아닙니다. 이미 take한 항목이 sleep 중 중단되면 로그와 큐 개수 모두에 남지 않을 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
정책 시작

비우기는 accepting=false, 즉시는 interrupt를 요청합니다.

  • 비우기: 새 submit 차단
  • 즉시: 새 submit 차단 검사 없음
  • 수용 차단과 현재 작업 취소는 다른 책임이다.
작업 처리

비우기는 java/thread 뒤 STOP, 즉시는 처리 중 누락 가능성이 있습니다.

  • 비우기: 두 문서 로그
  • 즉시 pending: 0·1·2 가능 / 처리 중 제외
  • 대기 큐와 실행 중 작업의 소유를 구분한다.
종료 회수

각 worker를 join합니다.

  • 비우기: 앞 문서 완료, 큐 비움
  • 즉시: 모든 문서 완료 보장 없음
  • 스레드 종료가 업무 전체 성공을 뜻하지 않는다.

미처리 항목은 호출자가 영속성 큐로 넘기도록 반환 타입을 확장할 수 있습니다.

pending=0도 모든 출력 완료를 뜻하지 않습니다. take로 꺼낸 문서가 sleep 중 취소되면 큐에도 완료 로그에도 없습니다. 이 구현의 submit에는 접수 차단 검사도 없어 stopNow 이후 문서를 넣을 수 있지만 종료한 작업자는 처리하지 않습니다. 실제 서비스에서는 큐 항목, 처리 중 항목, 완료 결과를 따로 기록하고 접수 종료 정책을 추가해야 합니다.


Empty·sleep·yield의 차이

반복문 본문이 비어 있는 while(condition){}는 바쁜 대기 spin입니다.

실행 기회를 받는 동안 CPU를 사용하며 조건 가시성이 보장되지 않으면 끝나지 않을 수 있습니다.

매우 짧은 락-free 대기에서만 Thread.onSpinWait()와 함께 제한적으로 사용하고 일반 큐 대기는 블로킹 기본형을 씁니다.

onSpinWait는 가시성을 만드는 연산이 아닙니다. 반복 조건에는 volatile이나 적절한 동기화가 별도로 필요합니다. sleep과 yield도 다른 스레드의 쓰기를 보이게 한다는 보장을 만들지 않습니다. 또한 synchronized 안에서 sleep한다고 가진 모니터를 내놓지 않으므로, 상대가 그 모니터를 얻어 조건을 바꿔야 하는 설계라면 대기를 추가하는 것만으로 해결되지 않습니다. JLS의 sleep·yield 규칙과 Thread API에서 두 성질을 확인할 수 있습니다.

Thread.sleep(duration)은 현재 스레드에 지정한 시간 동안의 대기를 요청합니다. 타이머와 스케줄러의 영향을 받고, interrupt로 대기가 일찍 끝날 수도 있습니다.

정확한 재개 시각과 다른 스레드 완료는 보장하지 않습니다.

중단 가능하므로 shutdown 통로가 됩니다.

Thread.yield()는 current 실행 가능 스레드가 스케줄러에 양보 의사를 알리는 힌트입니다.

무시될 수 있고 어떤 스레드가 다음인지 정하지 않습니다.

정확성, 공정성, 생산자·소비자 조정을 yield에 의존시키지 않습니다.


yield와 큐 프로토콜

작업을 여러 개 연속 처리한 뒤 yield()를 호출하면 다른 실행 가능 스레드가 기회를 얻을 수도 있습니다.

그러나 큐의 take()와 락이 이미 명확한 대기·소유권 규칙을 제공하며 yield()는 성능 실험 대상일 뿐입니다.

처리량이 떨어지면 제거할 수 있어야 합니다.

app/BatchedPrinterWorker.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class BatchedPrinterWorker {
    private sealed interface Command permits Print, Stop {}

    private record Print(String value) implements Command {}

    private record Stop() implements Command {}

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Command> queue = new LinkedBlockingQueue<>();
        Thread worker = new Thread(() -> consume(queue), "batch-printer");
        worker.start();
        queue.add(new Print("A"));
        queue.add(new Print("B"));
        queue.add(new Print("C"));
        queue.add(new Stop());
        worker.join();
        System.out.println("batch-complete");
    }

    private static void consume(BlockingQueue<Command> queue) {
        try {
            int batch = 0;
            for (; ; ) {
                Command command = queue.take();
                if (command instanceof Stop) return;
                Print print = (Print) command;
                System.out.println("printed=" + print.value());
                if (++batch % 2 == 0) Thread.yield();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

출력 A, B, C 순서는 FIFO 큐 때문에 고정되지 yield 때문이 아닙니다.

Stop도 같은 큐로 전달되므로 앞선 작업을 처리한 뒤 종료합니다.


shutdown API의 검증 목록

새 submit이 어느 시점부터 거부되는지, 큐의 대기 항목을 비울지, 진행 중인 항목이 원자적인지, interrupt가 블로킹 호출을 깨우는지, 소유자가 join하는지 확인합니다.

프로세스 종료 훅에서 호출할 때는 훅 자체의 시간 제한과 재진입도 고려합니다.

daemon 지정은 shutdown 구현을 대체하지 않습니다.

사용자 스레드가 프로세스를 붙잡는 문제와 안전하게 상태를 닫는 문제를 따로 해결합니다.

종료 로그는 요청 시각과 실제 작업자 종료 시각을 구분합니다.


연습 문제

큐에 세 작업을 넣고 poison marker로 비우기를 요청하되 100ms 안에 작업자가 끝나지 않으면 interrupt하세요.

메서드 반환 시 작업자는 살아 있지 않아야 합니다.

remaining을 세 작업 중 실패한 개수로 정의하지 마세요. 큐에는 종료 표식도 있고, take로 꺼냈지만 sleep 중 중단된 값은 큐 밖에 있습니다. 해설을 열기 전에 1을 꺼냈고 아직 done=1 로그는 없으며 큐에는 2·3·STOP이 남은 조건을 따로 그려 보세요. 이때 큐 항목 수와 아직 완료되지 않은 문서 수를 각각 센 뒤, 둘이 같은 숫자가 나와도 같은 대상을 센 것은 아닌 이유를 설명합니다.

정답과 두 단계 중단

먼저 정상 비우기 기회를 주고 시간 제한 join 뒤 생존하면 interrupt와 무기한 join으로 정리를 끝냅니다. 이 정리까지 100ms 안에 끝난다는 보장은 없습니다.

exercise/DeadlineDrainSolution.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class DeadlineDrainSolution {
    private static final int STOP = -1;

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        Thread worker =
                new Thread(
                        () -> {
                            try {
                                for (; ; ) {
                                    int value = queue.take();
                                    if (value == STOP) return;
                                    Thread.sleep(10);
                                    System.out.println("done=" + value);
                                }
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        },
                        "deadline-drain");
        worker.start();
        queue.add(1);
        queue.add(2);
        queue.add(3);
        queue.add(STOP);
        worker.join(100);
        if (worker.isAlive()) {
            worker.interrupt();
            worker.join();
        }
        System.out.println("stopped=" + !worker.isAlive() + ", remaining=" + queue.size());
    }
}

마지막 remaining에는 종료 표식도 포함될 수 있다

취소 중 가능한 remaining 3과 정상 drain remaining 0이라는 두 경로입니다. 큐 안/처리중/종료 관찰을 단계로 나누되 타이밍 결과를 고정하지 않아야 합니다.

마지막 remaining에는 종료 표식도 포함될 수 있다

DeadlineDrainSolution의 FIFO done 로그 앞부분과 stopped=true 뒤 remaining 값의 관계를 살펴봅니다.

SHUTDOWN · 큐와 처리 중 항목

마지막 remaining에는 종료 표식도 포함될 수 있다

remaining은 완료하지 못한 전체 작업 수가 아닙니다. STOP도 세고, 이미 꺼낸 값은 세지 않습니다.

큐 밖의 처리 중 항목과 STOP의 계수 가능한취소시점에서take한1은큐밖이고2·3·STOP만remaining3입니다. 다른가능정상경로는done1·2·3뒤STOP소비/remaining0입니다. 두결과를고정출력으로제시하지않고전체원표를보존합니다. 가능한 취소 시점 처음 큐에1·2·3·STOP 항목 수4 1을 꺼내 sleep 중 큐2·3·STOP / 수3 처리 중1은 큐 밖 그때 취소해 종료 done없음 / remaining3 꺼낸1도 미완료일 수 있음 가능한 정상 종료 1·2·3을 FIFO 처리 done1 / done2 / done3 STOP을 꺼내 종료 STOP로그는 없음 join 뒤 관측 stopped=true / remaining0 실선: 명시한 두 가능한 진행 remaining은 STOP포함 큐 크기 이 두 결과를 고정 출력이라 하지 않음
큐 밖의 처리 중 항목과 STOP의 계수 가능한취소시점에서take한1은큐밖이고2·3·STOP만remaining3입니다. 다른가능정상경로는done1·2·3뒤STOP소비/remaining0입니다. 두결과를고정출력으로제시하지않고전체원표를보존합니다. 가능한 취소 시점 처음 큐에1·2·3·STOP 항목 수4 1을 꺼내 sleep 중 큐2·3·STOP / 수3 처리 중1은 큐 밖 그때 취소해 종료 done없음 / remaining3 꺼낸1도 미완료일 수 있음 가능한 정상 종료 1·2·3을 FIFO 처리 done1 / done2 / done3 STOP을 꺼내 종료 STOP로그는 없음 join 뒤 관측 stopped=true / remaining0 실선: 명시한 두 가능한 진행 remaining은 STOP포함 큐 크기 이 두 결과를 고정 출력이라 하지 않음
마지막 remaining에는 종료 표식도 포함될 수 있다
출력된 done 로그가능한 remaining큐와 처리 중 값
없음3 또는 4첫 값을 꺼낸 뒤 중단되었을 수도 있음
done=12 또는 3다음 값을 꺼냈는지에 따라 달라짐
done=1, done=21 또는 2셋째 값이 처리 중이었을 수도 있음
done=1부터 done=3까지0 또는 1STOP까지 소비했으면 0 · 남았으면 1
없음
가능한 remaining
3 또는 4
큐와 처리 중 값
첫 값을 꺼낸 뒤 중단되었을 수도 있음
done=1
가능한 remaining
2 또는 3
큐와 처리 중 값
다음 값을 꺼냈는지에 따라 달라짐
done=1, done=2
가능한 remaining
1 또는 2
큐와 처리 중 값
셋째 값이 처리 중이었을 수도 있음
done=1부터 done=3까지
가능한 remaining
0 또는 1
큐와 처리 중 값
STOP까지 소비했으면 0 · 남았으면 1

모든 행은 마지막 stopped=true를 전제로 합니다. remaining은 STOP을 포함한 큐 항목 수이며 이미 꺼낸 값은 제외합니다. 100ms 뒤 취소로 전환할 수 있지만 이어지는 무기한 join까지 100ms 안에 끝난다는 뜻은 아닙니다.

이 예의 조건

  • 두 경로는 가능한 진행이며 고정 출력이 아니다.
  • remaining은 STOP을 포함한 큐 크기이다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2가능한 취소 시점에서 take한 1은 큐 밖이고 2·3·STOP만 remaining 3입니다. 다른 가능한 정상 경로는 done 1·2·3 뒤 STOP 소비/remaining 0입니다. 두 결과를 고정 출력으로 제시하지 않고 원래 전체 표를 보존합니다.
4처음 큐에 1·2·3·STOP
5항목 수 4
7큐 2·3·STOP
8수 3
9처리 중 1은 큐 밖
11done 없음
12remaining 3
13꺼낸 1도 미완료일 수 있음
16done1
17done2
18done3
20STOP 로그는 없음
23remaining 0
25remaining은 STOP 포함 큐 크기
원 도표의 설명과 주석
구분내용
설명가능한 취소 시점에서 take한 1은 큐 밖이고 2·3·STOP만 remaining 3입니다. 다른 가능한 정상 경로는 done 1·2·3 뒤 STOP 소비/remaining 0입니다. 두 결과를 고정 출력으로 제시하지 않고 원래 전체 표를 보존합니다.
전체 읽기 정보마지막 remaining에는 종료 표식도 포함될 수 있다 마지막 remaining에는 종료 표식도 포함될 수 있다 DeadlineDrainSolution의 FIFO done 로그 앞부분과 stopped=true 뒤 remaining 값의 관계를 살펴봅니다. SHUTDOWN · 큐와 처리 중 항목 마지막 remaining에는 종료 표식도 포함될 수 있다 remaining은 완료하지 못한 전체 작업 수가 아닙니다. STOP도 세고, 이미 꺼낸 값은 세지 않습니다. 마지막 remaining에는 종료 표식도 포함될 수 있다 출력된 done 로그 가능한 remaining 큐와 처리 중 값 없음 3 또는 4 첫 값을 꺼낸 뒤 중단되었을 수도 있음 done=1 2 또는 3 다음 값을 꺼냈는지에 따라 달라짐 done=1 , done=2 1 또는 2 셋째 값이 처리 중이었을 수도 있음 done=1 부터 done=3 까지 0 또는 1 STOP까지 소비했으면 0 · 남았으면 1 없음 가능한 remaining 3 또는 4 큐와 처리 중 값 첫 값을 꺼낸 뒤 중단되었을 수도 있음 done=1 가능한 remaining 2 또는 3 큐와 처리 중 값 다음 값을 꺼냈는지에 따라 달라짐 done=1 , done=2 가능한 remaining 1 또는 2 큐와 처리 중 값 셋째 값이 처리 중이었을 수도 있음 done=1 부터 done=3 까지 가능한 remaining 0 또는 1 큐와 처리 중 값 STOP까지 소비했으면 0 · 남았으면 1 모든 행은 마지막 stopped=true 를 전제로 합니다. remaining은 STOP을 포함한 큐 항목 수이며 이미 꺼낸 값은 제외합니다. 100ms 뒤 취소로 전환할 수 있지만 이어지는 무기한 join까지 100ms 안에 끝난다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 입력

1/2/3/STOP 네 항목을 넣습니다.

  • 큐: [1,2,3,STOP]
  • 크기: 4
  • STOP도 큐 멤버로 집계된다.
처리 중 경계

1을 take한 뒤 sleep 중에는 큐 밖에 있습니다.

  • 큐: [2,3,STOP]
  • remaining: 3
  • 처리 중: 1 미완료 가능
  • 큐 크기는 처리 중 작업을 세지 않는다.
대안 종료

이 시점 취소 또는 정상 FIFO 완료 뒤 join을 관찰합니다.

  • 취소 가능: done 없음,remaining 3
  • 정상 가능: done 1·2·3, STOP 소비,remaining 0
  • 회수: stopped=true
  • 어느 경로였는지 확인 없이 remaining을 전체 완료 수로 해석하지 않는다.

마지막 remaining에는 종료 표식도 포함될 수 있다

취소 중 가능한 remaining 3과 정상 drain remaining 0이라는 두 경로입니다. 큐 안/처리중/종료 관찰을 단계로 나누되 타이밍 결과를 고정하지 않아야 합니다.

마지막 remaining에는 종료 표식도 포함될 수 있다

DeadlineDrainSolution의 FIFO done 로그 앞부분과 stopped=true 뒤 remaining 값의 관계를 살펴봅니다.

SHUTDOWN · 큐와 처리 중 항목

마지막 remaining에는 종료 표식도 포함될 수 있다

remaining은 완료하지 못한 전체 작업 수가 아닙니다. STOP도 세고, 이미 꺼낸 값은 세지 않습니다.

큐 밖의 처리 중 항목과 STOP의 계수 가능한취소시점에서take한1은큐밖이고2·3·STOP만remaining3입니다. 다른가능정상경로는done1·2·3뒤STOP소비/remaining0입니다. 두결과를고정출력으로제시하지않고전체원표를보존합니다. 가능한 취소 시점 처음 큐에1·2·3·STOP 항목 수4 1을 꺼내 sleep 중 큐2·3·STOP / 수3 처리 중1은 큐 밖 그때 취소해 종료 done없음 / remaining3 꺼낸1도 미완료일 수 있음 가능한 정상 종료 1·2·3을 FIFO 처리 done1 / done2 / done3 STOP을 꺼내 종료 STOP로그는 없음 join 뒤 관측 stopped=true / remaining0 실선: 명시한 두 가능한 진행 remaining은 STOP포함 큐 크기 이 두 결과를 고정 출력이라 하지 않음
큐 밖의 처리 중 항목과 STOP의 계수 가능한취소시점에서take한1은큐밖이고2·3·STOP만remaining3입니다. 다른가능정상경로는done1·2·3뒤STOP소비/remaining0입니다. 두결과를고정출력으로제시하지않고전체원표를보존합니다. 가능한 취소 시점 처음 큐에1·2·3·STOP 항목 수4 1을 꺼내 sleep 중 큐2·3·STOP / 수3 처리 중1은 큐 밖 그때 취소해 종료 done없음 / remaining3 꺼낸1도 미완료일 수 있음 가능한 정상 종료 1·2·3을 FIFO 처리 done1 / done2 / done3 STOP을 꺼내 종료 STOP로그는 없음 join 뒤 관측 stopped=true / remaining0 실선: 명시한 두 가능한 진행 remaining은 STOP포함 큐 크기 이 두 결과를 고정 출력이라 하지 않음
마지막 remaining에는 종료 표식도 포함될 수 있다
출력된 done 로그가능한 remaining큐와 처리 중 값
없음3 또는 4첫 값을 꺼낸 뒤 중단되었을 수도 있음
done=12 또는 3다음 값을 꺼냈는지에 따라 달라짐
done=1, done=21 또는 2셋째 값이 처리 중이었을 수도 있음
done=1부터 done=3까지0 또는 1STOP까지 소비했으면 0 · 남았으면 1
없음
가능한 remaining
3 또는 4
큐와 처리 중 값
첫 값을 꺼낸 뒤 중단되었을 수도 있음
done=1
가능한 remaining
2 또는 3
큐와 처리 중 값
다음 값을 꺼냈는지에 따라 달라짐
done=1, done=2
가능한 remaining
1 또는 2
큐와 처리 중 값
셋째 값이 처리 중이었을 수도 있음
done=1부터 done=3까지
가능한 remaining
0 또는 1
큐와 처리 중 값
STOP까지 소비했으면 0 · 남았으면 1

모든 행은 마지막 stopped=true를 전제로 합니다. remaining은 STOP을 포함한 큐 항목 수이며 이미 꺼낸 값은 제외합니다. 100ms 뒤 취소로 전환할 수 있지만 이어지는 무기한 join까지 100ms 안에 끝난다는 뜻은 아닙니다.

이 예의 조건

  • 두 경로는 가능한 진행이며 고정 출력이 아니다.
  • remaining은 STOP을 포함한 큐 크기이다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2가능한 취소 시점에서 take한 1은 큐 밖이고 2·3·STOP만 remaining 3입니다. 다른 가능한 정상 경로는 done 1·2·3 뒤 STOP 소비/remaining 0입니다. 두 결과를 고정 출력으로 제시하지 않고 원래 전체 표를 보존합니다.
4처음 큐에 1·2·3·STOP
5항목 수 4
7큐 2·3·STOP
8수 3
9처리 중 1은 큐 밖
11done 없음
12remaining 3
13꺼낸 1도 미완료일 수 있음
16done1
17done2
18done3
20STOP 로그는 없음
23remaining 0
25remaining은 STOP 포함 큐 크기
원 도표의 설명과 주석
구분내용
설명가능한 취소 시점에서 take한 1은 큐 밖이고 2·3·STOP만 remaining 3입니다. 다른 가능한 정상 경로는 done 1·2·3 뒤 STOP 소비/remaining 0입니다. 두 결과를 고정 출력으로 제시하지 않고 원래 전체 표를 보존합니다.
전체 읽기 정보마지막 remaining에는 종료 표식도 포함될 수 있다 마지막 remaining에는 종료 표식도 포함될 수 있다 DeadlineDrainSolution의 FIFO done 로그 앞부분과 stopped=true 뒤 remaining 값의 관계를 살펴봅니다. SHUTDOWN · 큐와 처리 중 항목 마지막 remaining에는 종료 표식도 포함될 수 있다 remaining은 완료하지 못한 전체 작업 수가 아닙니다. STOP도 세고, 이미 꺼낸 값은 세지 않습니다. 마지막 remaining에는 종료 표식도 포함될 수 있다 출력된 done 로그 가능한 remaining 큐와 처리 중 값 없음 3 또는 4 첫 값을 꺼낸 뒤 중단되었을 수도 있음 done=1 2 또는 3 다음 값을 꺼냈는지에 따라 달라짐 done=1 , done=2 1 또는 2 셋째 값이 처리 중이었을 수도 있음 done=1 부터 done=3 까지 0 또는 1 STOP까지 소비했으면 0 · 남았으면 1 없음 가능한 remaining 3 또는 4 큐와 처리 중 값 첫 값을 꺼낸 뒤 중단되었을 수도 있음 done=1 가능한 remaining 2 또는 3 큐와 처리 중 값 다음 값을 꺼냈는지에 따라 달라짐 done=1 , done=2 가능한 remaining 1 또는 2 큐와 처리 중 값 셋째 값이 처리 중이었을 수도 있음 done=1 부터 done=3 까지 가능한 remaining 0 또는 1 큐와 처리 중 값 STOP까지 소비했으면 0 · 남았으면 1 모든 행은 마지막 stopped=true 를 전제로 합니다. remaining은 STOP을 포함한 큐 항목 수이며 이미 꺼낸 값은 제외합니다. 100ms 뒤 취소로 전환할 수 있지만 이어지는 무기한 join까지 100ms 안에 끝난다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 입력

1/2/3/STOP 네 항목을 넣습니다.

  • 큐: [1,2,3,STOP]
  • 크기: 4
  • STOP도 큐 멤버로 집계된다.
처리 중 경계

1을 take한 뒤 sleep 중에는 큐 밖에 있습니다.

  • 큐: [2,3,STOP]
  • remaining: 3
  • 처리 중: 1 미완료 가능
  • 큐 크기는 처리 중 작업을 세지 않는다.
대안 종료

이 시점 취소 또는 정상 FIFO 완료 뒤 join을 관찰합니다.

  • 취소 가능: done 없음,remaining 3
  • 정상 가능: done 1·2·3, STOP 소비,remaining 0
  • 회수: stopped=true
  • 어느 경로였는지 확인 없이 remaining을 전체 완료 수로 해석하지 않는다.

graceful shutdown은 플래그 하나나 yield 힌트가 아닙니다.

입력 접수 차단, 큐 정책, interrupt 가능한 대기, 시간 제한 전환, 최종 join을 한 소유자가 책임질 때 종료 상태를 예측할 수 있습니다.

앞의 연습에서 수를 세는 기준도 확인하세요. 1은 꺼냈지만 아직 완료하지 않았고 큐에는 2·3·STOP이 있다면 큐 항목은 3개, 미완료 문서도 1·2·3의 3개입니다. 숫자만 우연히 같고 전자는 STOP을 포함하면서 처리 중인 1을 제외합니다. 또한 join(100)의 예산은 그 호출에서 시작하며 앞선 시작·입력 등록 시간까지 합친 전체 제한이 아닙니다. 취소로 전환한 뒤의 무기한 join은 별도 정리 단계입니다.


종료 계약의 경계 확인하기

각 상황은 독립적이며 외부 예외·추가 interrupt는 없다고 가정합니다.

  1. 아직 start하지 않은 DrainingPrinter에 "A"를 submit하고 shutdown을 호출했습니다. join 반환으로 A 출력 완료를 판단할 수 있나요? 나중에 start하면 어떤 순서로 처리되며, 같은 Thread에 start를 다시 호출할 수 있나요?
  2. 1을 take한 작업자가 완료 로그 전에 중단되어 종료했습니다. 큐에는 2·3·STOP만 남았습니다. remaining, 미완료 문서 수, 미완료 문서 집합은 각각 무엇인가요? 모든 done=1·2·3 로그는 나왔지만 STOP을 꺼내기 전에 종료한 별도 상황에서는 remaining과 미완료 문서 수가 얼마인가요?
  3. 시작·입력 등록에 30ms를 쓴 뒤 join(100)을 호출해 끝까지 기다렸고 정리에 40ms가 더 들었다고 가정합니다. 이 관측의 전체 소요 시간은 얼마인가요? join(100)이나 yield가 전체 반환 시간을 100ms로 제한하거나 다른 작업자의 진척을 보장하나요?
시작 순서·항목 계수·시간 예산의 해설
  1. 판단할 수 없습니다. 시작 전 join은 즉시 반환하고 큐에는 A·고유 STOP이 남아 있습니다. 나중에 처음 start하면 정상 경로에서 A를 출력한 뒤 STOP에서 종료하지만, 앞의 shutdown 반환이 그 완료를 회수한 것은 아닙니다. 두 번째 start는 IllegalThreadStateException이므로 객체 재사용을 위해 같은 Thread를 다시 시작할 수 없습니다.
  2. remaining=3이고 미완료 문서 수는 3, 집합은 1·2·3입니다. 큐의 셋은 2·3·STOP으로 서로 다른 집합입니다. 별도 상황에서는 remaining=1(STOP)이고 미완료 문서는 0입니다. 종료 표식은 업무 문서가 아닙니다.
  3. 명시한 관측 시간의 합은 30+100+40=170ms입니다. 이 계산은 고정한 관측값의 합이며 실제 타이머·스케줄링이 정확히 그 시간을 보장하지는 않습니다. join의 대기 예산과 앞뒤 작업은 별개이며 무기한 정리가 붙습니다. yield는 무시할 수 있는 힌트이므로 시간 제한이나 다른 작업자 진척 보장에 쓸 수 없습니다.