본문 바로가기

interrupt 플래그와 취소

interrupted와 isInterrupted의 차이, InterruptedException의 플래그 소거, 큐 기반 printer 취소를 실행으로 확인합니다.

interrupt는 Thread 안에 있는 상태와 중단 가능 API의 예외 경로가 함께 만드는 프로토콜입니다.

플래그를 읽는 메서드마다 소거 여부가 다르고, sleep·wait·join이 중단되어 던지는 InterruptedException도 플래그를 지웁니다.

이 규칙을 모르면 한 계층에서 취소 요청을 소비해 상위 코드가 계속 일하게 됩니다.


interrupted 플래그 소비

정적 Thread.interrupted()는 current 스레드의 플래그를 반환한 뒤 false로 지웁니다.

아래 프로그램은 자기 자신을 interrupt하고 두 번 읽으므로 실제 출력은 first=true, second=false입니다.

두 번째도 true일 것이라는 기대가 잘못됐습니다.

lab/InterruptedFlagClearedBug.java
public final class InterruptedFlagClearedBug {
    public static void main(String[] args) {
        Thread.currentThread().interrupt();
        boolean first = Thread.interrupted();
        boolean second = Thread.interrupted();
        System.out.println("first=" + first + ", second=" + second);
        System.out.println("wrong-assumption=" + (first == second));
    }
}

반면 current.isInterrupted()는 지정한 Thread의 플래그를 지우지 않고 조회합니다.

호출 표기와 조회 대상을 혼동하지 마세요. interrupted()는 정적 메서드이므로 worker.interrupted()처럼 객체로 적어도 현재 실행 중인 스레드의 플래그를 지웁니다. 작업자 상태를 보려면 worker.isInterrupted()를 쓰고, 현재 요청을 소비할 때는 Thread.interrupted()로 적는 편이 의도가 분명합니다. Thread API에서 두 메서드의 대상과 소거 규칙을 확인할 수 있습니다.

자기 플래그에 한 번만 interrupt하고 새로운 요청은 없다고 가정하세요. isInterrupted를 두 번 먼저 읽으면 두 번 모두 true이고, 그 뒤 interrupted를 두 번 읽으면 true·false입니다. 앞의 두 조회는 보관 상태를 바꾸지 않고 세 번째 조회가 요청을 소비합니다. 새 interrupt가 사이에 들어오면 이 단순 순서 계산의 전제가 달라집니다.

반복문 조건에서 상태를 유지하며 보고 싶으면 인스턴스 메서드가 맞고, 현재 계층에서 요청을 소비하기로 했다면 static 메서드의 소거 의미를 의도적으로 사용할 수 있습니다.


interrupt 요청이 전달되는 세 경로

RUNNABLE 계산 스레드에 interrupt하면 플래그가 true가 되고 코드는 확인할 때까지 계속 실행합니다.

sleep·wait·join 중이면 대기가 풀리고 InterruptedException이 발생하며 플래그는 false로 정리됩니다.

이미 플래그가 true인 상태에서 중단 가능 블로킹 호출로 들어가도 즉시 예외가 날 수 있습니다.

catch에서 작업을 끝내고 반환한다면 플래그 복원이 반드시 필요한 것은 아닙니다.

하지만 라이브러리 계층이 정리만 하고 예외를 던질 수 없어서 반환한다면 Thread.currentThread().interrupt()로 상위에 신호를 보존하는 관례가 중요합니다.

아무 처리 없이 catch를 비우는 것이 가장 위험합니다.

src/InterruptedExceptionRestoration.java
public final class InterruptedExceptionRestoration {
    public static void main(String[] args) throws InterruptedException {
        Thread worker =
                new Thread(
                        () -> {
                            boolean completed = waitForInput();
                            System.out.println(
                                    "completed="
                                            + completed
                                            + ", restored="
                                            + Thread.currentThread().isInterrupted());
                        },
                        "restore-worker");
        worker.start();
        Thread.sleep(10);
        worker.interrupt();
        worker.join();
    }

    private static boolean waitForInput() {
        try {
            Thread.sleep(5_000);
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

플래그 조회·소거·복원은 서로 다른 동작이다

interrupted 조회가 true 플래그를 지우고 다음 조회 false, 별도 대기 중단 catch가 복원합니다. 같은 플래그의 상태 변화를 순서대로 확인할 수 있습니다.

플래그 조회·소거·복원은 서로 다른 동작이다

자기 interrupt를 두 번 소비하는 원문과 InterruptedException 처리 예제의 조건을 구분합니다.

INTERRUPT · 조회 / 소거 / 복원

플래그 조회·소거·복원은 서로 다른 동작이다

isInterrupted는 그대로 조회하고, interrupted는 현재 플래그를 지웁니다. 중단 예외 처리 뒤 복원 여부는 정책입니다.

소거되는 플래그와 복원 정책 자기interrupt뒤첫interrupted=true/소거,둘째false이며대기중중단예외에서는소거뒤정책에따라복원합니다. isInterrupted는소거하지않고두예제의조건을분리합니다. 자기 interrupt 예제 현재 스레드에 요청 플래그 true 첫 interrupted 조회 true 반환 / 소거 두 번째 interrupted false / 새 요청 없을 때 대기 중 중단 처리 sleep에서 중단 예외 플래그 소거됨 catch에서 interrupt 정책에 따라 true로 복원 실선: 각 플래그의 변화 isInterrupted 조회는 지우지 않음 두 영역은 별도 예제 조건
소거되는 플래그와 복원 정책 자기interrupt뒤첫interrupted=true/소거,둘째false이며대기중중단예외에서는소거뒤정책에따라복원합니다. isInterrupted는소거하지않고두예제의조건을분리합니다. 자기 interrupt 예제 현재 스레드에 요청 플래그 true 첫 interrupted 조회 true 반환 / 소거 두 번째 interrupted false / 새 요청 없을 때 대기 중 중단 처리 sleep에서 중단 예외 플래그 소거됨 catch에서 interrupt 정책에 따라 true로 복원 실선: 각 플래그의 변화 isInterrupted 조회는 지우지 않음 두 영역은 별도 예제 조건

t.isInterrupted()

플래그에 미치는 영향
지정한 스레드의 값을 지우지 않고 조회
이 문서의 근거
API 규칙 · 상대 스레드를 종료시키지 않음

Thread.interrupted() 두 번

플래그에 미치는 영향
현재 플래그를 읽고 매번 지움
이 문서의 근거
자기 interrupt 예제: first=true, second=false

sleep 대기에서 중단 예외

플래그에 미치는 영향
InterruptedException과 함께 플래그 소거
이 문서의 근거
예외를 처리하는 경로의 규칙

catch에서 다시 interrupt()

플래그에 미치는 영향
현재 스레드의 플래그를 복원
이 문서의 근거
대기 중 중단된 경우 completed=false, restored=true

복원 예제의 main sleep 10ms는 작업자 sleep 5000ms와의 순서를 강제하지 않습니다. 작업자가 먼저 정상 대기를 끝내면 completed=true도 가능하며, restored 값도 interrupt 시점에 따라 달라집니다. 첫 자기 interrupt 예제의 두 값은 고정됩니다.

이 예의 조건

  • 자기 interrupt 조회와 sleep 중단 복원은 독립 실행입니다. 각 장면에서 같은 관찰 지점을 비교합니다.
  • isInterrupted는 조회로 플래그를 소거하지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2자기 interrupt 뒤 첫 interrupted는 true/소거, 둘째는 false이며 대기 중 중단 예외에서는 소거 뒤 정책에 따라 복원합니다. isInterrupted는 소거하지 않고 두 예제의 조건을 분리합니다.
7true 반환
8소거
10false
11새 요청 없을 때
원 도표의 설명과 주석
구분내용
설명자기 interrupt 뒤 첫 interrupted는 true/소거, 둘째는 false이며 대기 중 중단 예외에서는 소거 뒤 정책에 따라 복원합니다. isInterrupted는 소거하지 않고 두 예제의 조건을 분리합니다.
전체 읽기 정보플래그 조회·소거·복원은 서로 다른 동작이다 플래그 조회·소거·복원은 서로 다른 동작이다 자기 interrupt를 두 번 소비하는 원문과 InterruptedException 처리 예제의 조건을 구분합니다. INTERRUPT · 조회 / 소거 / 복원 플래그 조회·소거·복원은 서로 다른 동작이다 isInterrupted는 그대로 조회하고, interrupted는 현재 플래그를 지웁니다. 중단 예외 처리 뒤 복원 여부는 정책입니다. t.isInterrupted() 플래그에 미치는 영향 지정한 스레드의 값을 지우지 않고 조회 이 문서의 근거 API 규칙 · 상대 스레드를 종료시키지 않음 Thread.interrupted() 두 번 플래그에 미치는 영향 현재 플래그를 읽고 매번 지움 이 문서의 근거 자기 interrupt 예제: first=true , second=false sleep 대기에서 중단 예외 플래그에 미치는 영향 InterruptedException 과 함께 플래그 소거 이 문서의 근거 예외를 처리하는 경로의 규칙 catch에서 다시 interrupt() 플래그에 미치는 영향 현재 스레드의 플래그를 복원 이 문서의 근거 대기 중 중단된 경우 completed=false , restored=true 복원 예제의 main sleep 10ms는 작업자 sleep 5000ms와의 순서를 강제하지 않습니다. 작업자가 먼저 정상 대기를 끝내면 completed=true도 가능하며, restored 값도 interrupt 시점에 따라 달라집니다. 첫 자기 interrupt 예제의 두 값은 고정됩니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요청과 대기 조건

자기 조회 실행과 sleep 중단 실행의 시작 조건을 나란히 둡니다.

  • 자기 조회 실행: 현재 스레드에 interrupt, 플래그 true
  • sleep 실행: 대기 중 interrupt 요청, 중단 예외 처리 대상
  • 같은 중단 요청도 조회와 대기 API에서 다르게 관찰됩니다.
소거가 일어나는 지점

각 독립 실행에서 플래그가 소거되는 경계를 비교합니다.

  • 자기 조회 실행: 첫 interrupted=true, 플래그 소거
  • sleep 실행: InterruptedException 수신 때 플래그 소거
  • 소거 조회의 반환값과 대기 중단 예외를 하나의 실행 순서로 연결하지 않습니다.
후속 조회와 복원

소거 뒤 새 요청 없는 조회와 catch의 복원 정책을 대조합니다.

  • 자기 조회 실행: 두 번째 interrupted=false
  • sleep 실행: catch에서 interrupt 호출, 플래그 true로 복원
  • false 조회는 새 요청 부재, true 복원은 명시적 재요청의 결과입니다.

플래그 조회·소거·복원은 서로 다른 동작이다

interrupted 조회가 true 플래그를 지우고 다음 조회 false, 별도 대기 중단 catch가 복원합니다. 같은 플래그의 상태 변화를 순서대로 확인할 수 있습니다.

플래그 조회·소거·복원은 서로 다른 동작이다

자기 interrupt를 두 번 소비하는 원문과 InterruptedException 처리 예제의 조건을 구분합니다.

INTERRUPT · 조회 / 소거 / 복원

플래그 조회·소거·복원은 서로 다른 동작이다

isInterrupted는 그대로 조회하고, interrupted는 현재 플래그를 지웁니다. 중단 예외 처리 뒤 복원 여부는 정책입니다.

소거되는 플래그와 복원 정책 자기interrupt뒤첫interrupted=true/소거,둘째false이며대기중중단예외에서는소거뒤정책에따라복원합니다. isInterrupted는소거하지않고두예제의조건을분리합니다. 자기 interrupt 예제 현재 스레드에 요청 플래그 true 첫 interrupted 조회 true 반환 / 소거 두 번째 interrupted false / 새 요청 없을 때 대기 중 중단 처리 sleep에서 중단 예외 플래그 소거됨 catch에서 interrupt 정책에 따라 true로 복원 실선: 각 플래그의 변화 isInterrupted 조회는 지우지 않음 두 영역은 별도 예제 조건
소거되는 플래그와 복원 정책 자기interrupt뒤첫interrupted=true/소거,둘째false이며대기중중단예외에서는소거뒤정책에따라복원합니다. isInterrupted는소거하지않고두예제의조건을분리합니다. 자기 interrupt 예제 현재 스레드에 요청 플래그 true 첫 interrupted 조회 true 반환 / 소거 두 번째 interrupted false / 새 요청 없을 때 대기 중 중단 처리 sleep에서 중단 예외 플래그 소거됨 catch에서 interrupt 정책에 따라 true로 복원 실선: 각 플래그의 변화 isInterrupted 조회는 지우지 않음 두 영역은 별도 예제 조건

t.isInterrupted()

플래그에 미치는 영향
지정한 스레드의 값을 지우지 않고 조회
이 문서의 근거
API 규칙 · 상대 스레드를 종료시키지 않음

Thread.interrupted() 두 번

플래그에 미치는 영향
현재 플래그를 읽고 매번 지움
이 문서의 근거
자기 interrupt 예제: first=true, second=false

sleep 대기에서 중단 예외

플래그에 미치는 영향
InterruptedException과 함께 플래그 소거
이 문서의 근거
예외를 처리하는 경로의 규칙

catch에서 다시 interrupt()

플래그에 미치는 영향
현재 스레드의 플래그를 복원
이 문서의 근거
대기 중 중단된 경우 completed=false, restored=true

복원 예제의 main sleep 10ms는 작업자 sleep 5000ms와의 순서를 강제하지 않습니다. 작업자가 먼저 정상 대기를 끝내면 completed=true도 가능하며, restored 값도 interrupt 시점에 따라 달라집니다. 첫 자기 interrupt 예제의 두 값은 고정됩니다.

이 예의 조건

  • 자기 interrupt 조회와 sleep 중단 복원은 독립 실행입니다. 각 장면에서 같은 관찰 지점을 비교합니다.
  • isInterrupted는 조회로 플래그를 소거하지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2자기 interrupt 뒤 첫 interrupted는 true/소거, 둘째는 false이며 대기 중 중단 예외에서는 소거 뒤 정책에 따라 복원합니다. isInterrupted는 소거하지 않고 두 예제의 조건을 분리합니다.
7true 반환
8소거
10false
11새 요청 없을 때
원 도표의 설명과 주석
구분내용
설명자기 interrupt 뒤 첫 interrupted는 true/소거, 둘째는 false이며 대기 중 중단 예외에서는 소거 뒤 정책에 따라 복원합니다. isInterrupted는 소거하지 않고 두 예제의 조건을 분리합니다.
전체 읽기 정보플래그 조회·소거·복원은 서로 다른 동작이다 플래그 조회·소거·복원은 서로 다른 동작이다 자기 interrupt를 두 번 소비하는 원문과 InterruptedException 처리 예제의 조건을 구분합니다. INTERRUPT · 조회 / 소거 / 복원 플래그 조회·소거·복원은 서로 다른 동작이다 isInterrupted는 그대로 조회하고, interrupted는 현재 플래그를 지웁니다. 중단 예외 처리 뒤 복원 여부는 정책입니다. t.isInterrupted() 플래그에 미치는 영향 지정한 스레드의 값을 지우지 않고 조회 이 문서의 근거 API 규칙 · 상대 스레드를 종료시키지 않음 Thread.interrupted() 두 번 플래그에 미치는 영향 현재 플래그를 읽고 매번 지움 이 문서의 근거 자기 interrupt 예제: first=true , second=false sleep 대기에서 중단 예외 플래그에 미치는 영향 InterruptedException 과 함께 플래그 소거 이 문서의 근거 예외를 처리하는 경로의 규칙 catch에서 다시 interrupt() 플래그에 미치는 영향 현재 스레드의 플래그를 복원 이 문서의 근거 대기 중 중단된 경우 completed=false , restored=true 복원 예제의 main sleep 10ms는 작업자 sleep 5000ms와의 순서를 강제하지 않습니다. 작업자가 먼저 정상 대기를 끝내면 completed=true도 가능하며, restored 값도 interrupt 시점에 따라 달라집니다. 첫 자기 interrupt 예제의 두 값은 고정됩니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요청과 대기 조건

자기 조회 실행과 sleep 중단 실행의 시작 조건을 나란히 둡니다.

  • 자기 조회 실행: 현재 스레드에 interrupt, 플래그 true
  • sleep 실행: 대기 중 interrupt 요청, 중단 예외 처리 대상
  • 같은 중단 요청도 조회와 대기 API에서 다르게 관찰됩니다.
소거가 일어나는 지점

각 독립 실행에서 플래그가 소거되는 경계를 비교합니다.

  • 자기 조회 실행: 첫 interrupted=true, 플래그 소거
  • sleep 실행: InterruptedException 수신 때 플래그 소거
  • 소거 조회의 반환값과 대기 중단 예외를 하나의 실행 순서로 연결하지 않습니다.
후속 조회와 복원

소거 뒤 새 요청 없는 조회와 catch의 복원 정책을 대조합니다.

  • 자기 조회 실행: 두 번째 interrupted=false
  • sleep 실행: catch에서 interrupt 호출, 플래그 true로 복원
  • false 조회는 새 요청 부재, true 복원은 명시적 재요청의 결과입니다.

도우미가 검사 예외를 시그니처에 올릴 수 있다면 catch하지 않고 호출자에게 전달하는 쪽이 더 단순할 수 있습니다.


블로킹 큐의 interrupt 대기

printer가 큐를 계속 확인하며 비어 있으면 sleep하는 구조는 지연과 불필요한 wake-up을 만듭니다.

이 예제의 LinkedBlockingQueue.take()는 항목이 없으면 기다리고 interrupt에 응답합니다.

생산자는 put 또는 offer로 작업을 전달합니다.

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

public final class InterruptiblePrinter {
    private final BlockingQueue<String> jobs = new LinkedBlockingQueue<>();
    private final Thread worker = new Thread(this::loop, "printer-worker");

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

    public void submit(String document) {
        jobs.add(document);
    }

    public void stop() throws InterruptedException {
        worker.interrupt();
        worker.join();
    }

    private void loop() {
        try {
            while (true) {
                String job = jobs.take();
                System.out.println("printed=" + job);
            }
        } catch (InterruptedException e) {
            System.out.println("printer-cancelled pending=" + jobs.size());
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptiblePrinter printer = new InterruptiblePrinter();
        printer.start();
        printer.submit("post-18");
        while (!printer.jobs.isEmpty()) {
            Thread.onSpinWait();
        }
        printer.stop();
    }
}

큐가 빈 시점과 출력이 끝난 시점은 다르다

한 항목 submit→take로 큐 밖 처리→다음 take 중단→join 회수의 실제 수명입니다. 큐 empty와 출력 완료를 단계별로 구분해 처리 완료 시점을 확인할 수 있습니다.

큐가 빈 시점과 출력이 끝난 시점은 다르다

InterruptiblePrinter의 post-18 하나를 넣고 큐가 빈 것을 확인한 뒤 interrupt와 join하는 실제 흐름을 읽습니다.

PRINTER · 큐와 완료 경계

큐가 빈 시점과 출력이 끝난 시점은 다르다

isEmpty는 항목을 꺼냈다는 사실입니다. 이 예제는 콘솔 로그 뒤 다음 take에서 중단되고 join으로 회수됩니다.

큐에서 꺼낸 항목과 마지막 회수take뒤빈큐와처리중post-18을구별합니다. 원코드는콘솔로그뒤다음take에서취소를받고join으로회수하며main의interrupt와콘솔로그앞뒤는고정하지않습니다.
submit(post-18)큐에 한 항목 등록
연결 관계submit(post-18) · 큐에 한 항목 등록 → worker가 take · 큐 빈 상태 / 처리 중 항목
worker가 take큐 빈 상태 / 처리 중 항목
연결 관계worker가 take · 큐 빈 상태 / 처리 중 항목 → 콘솔 printed=post-18 · main 취소와 앞뒤는 가변
콘솔 printed=post-18main 취소와 앞뒤는 가변
연결 관계콘솔 printed=post-18 · main 취소와 앞뒤는 가변 → 다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신
다음 take의 중단interrupt 요청을중단 예외로 수신
연결 관계다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신 → main의 join 정상 반환 · printer-cancelled · pending=0
main의 join 정상 반환printer-cancelledpending=0

실선: 항목 처리와 마지막 종료의 관계isEmpty는 출력 완료 신호 아님interrupt즉시 종료를 뜻하지 않음

큐에서 꺼낸 항목과 마지막 회수take뒤빈큐와처리중post-18을구별합니다. 원코드는콘솔로그뒤다음take에서취소를받고join으로회수하며main의interrupt와콘솔로그앞뒤는고정하지않습니다.
submit(post-18)큐에 한 항목 등록
연결 관계submit(post-18) · 큐에 한 항목 등록 → worker가 take · 큐 빈 상태 / 처리 중 항목
worker가 take큐 빈 상태 / 처리 중 항목
연결 관계worker가 take · 큐 빈 상태 / 처리 중 항목 → 콘솔 printed=post-18 · main 취소와 앞뒤는 가변
콘솔 printed=post-18main 취소와 앞뒤는 가변
연결 관계콘솔 printed=post-18 · main 취소와 앞뒤는 가변 → 다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신
다음 take의 중단interrupt 요청을중단 예외로 수신
연결 관계다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신 → main의 join 정상 반환 · printer-cancelled · pending=0
main의 join 정상 반환printer-cancelledpending=0

실선: 항목 처리와 마지막종료의 관계isEmpty는 출력 완료 신호아님interrupt즉시 종료를 뜻하지않음

  1. submit("post-18")

    확인된 사실
    항목 하나를 큐에 넣음
    아직 뜻하지 않는 것
    출력 완료
  2. jobs.isEmpty() 확인

    확인된 사실
    소비자가 항목을 꺼냄
    아직 뜻하지 않는 것
    콘솔 출력 완료
  3. interrupt() 요청

    확인된 사실
    다음 중단 가능한 대기에 취소를 전달
    아직 뜻하지 않는 것
    요청 즉시 작업자 종료
  4. join() 반환

    확인된 사실
    작업자 종료까지 회수
    아직 뜻하지 않는 것
    새 submit을 거부하는 수용 정책

이 원문은 printed=post-18 다음 printer-cancelled pending=0을 남깁니다. 꺼낸 항목의 콘솔 로그 뒤 다음 take가 중단됩니다. 큐 확인 반복에는 자체 시간 제한이 없고, drain·running 플래그·Outcome은 이 클래스에 없습니다.

이 예의 조건

  • main의 interrupt와 콘솔 출력의 앞뒤는 고정되지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2take 뒤 빈 큐와 처리 중 post-18을 구별합니다. 원래 코드는 콘솔 로그 뒤 다음 take에서 취소를 받고 join으로 회수하며 main의 interrupt와 콘솔 로그 앞뒤는 고정하지 않습니다.
6큐 빈 상태
7처리 중 항목
18interrupt 즉시 종료를 뜻하지 않음
원 도표의 설명과 주석
구분내용
설명take 뒤 빈 큐와 처리 중 post-18을 구별합니다. 원래 코드는 콘솔 로그 뒤 다음 take에서 취소를 받고 join으로 회수하며 main의 interrupt와 콘솔 로그 앞뒤는 고정하지 않습니다.
전체 읽기 정보큐가 빈 시점과 출력이 끝난 시점은 다르다 큐가 빈 시점과 출력이 끝난 시점은 다르다 InterruptiblePrinter의 post-18 하나를 넣고 큐가 빈 것을 확인한 뒤 interrupt와 join하는 실제 흐름을 읽습니다. PRINTER · 큐와 완료 경계 큐가 빈 시점과 출력이 끝난 시점은 다르다 isEmpty는 항목을 꺼냈다는 사실입니다. 이 예제는 콘솔 로그 뒤 다음 take에서 중단되고 join으로 회수됩니다. submit("post-18") 확인된 사실 항목 하나를 큐에 넣음 아직 뜻하지 않는 것 출력 완료 jobs.isEmpty() 확인 확인된 사실 소비자가 항목을 꺼냄 아직 뜻하지 않는 것 콘솔 출력 완료 interrupt() 요청 확인된 사실 다음 중단 가능한 대기에 취소를 전달 아직 뜻하지 않는 것 요청 즉시 작업자 종료 join() 반환 확인된 사실 작업자 종료까지 회수 아직 뜻하지 않는 것 새 submit을 거부하는 수용 정책 이 원문은 printed=post-18 다음 printer-cancelled pending=0 을 남깁니다. 꺼낸 항목의 콘솔 로그 뒤 다음 take가 중단됩니다. 큐 확인 반복에는 자체 시간 제한이 없고, drain·running 플래그·Outcome은 이 클래스에 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
항목 등록

submit(post-18)이 항목을 큐에 넣습니다.

  • 큐: post-18
  • 소유: 아직 큐
  • worker의 take가 항목 소유를 가져갑니다.
처리 중

take 뒤 큐는 비어 있고 worker가 출력할 항목을 보유합니다.

  • 큐: 빈 큐
  • 처리: printed=post-18
  • isEmpty만으로 출력 완료를 판정할 수 없습니다.
취소 회수

다음 take가 중단 예외를 받고 worker가 종료합니다.

  • join 뒤: printer-cancelled
  • pending: 0
  • main의 정상 join이 종료를 회수합니다.

큐가 빈 시점과 출력이 끝난 시점은 다르다

한 항목 submit→take로 큐 밖 처리→다음 take 중단→join 회수의 실제 수명입니다. 큐 empty와 출력 완료를 단계별로 구분해 처리 완료 시점을 확인할 수 있습니다.

큐가 빈 시점과 출력이 끝난 시점은 다르다

InterruptiblePrinter의 post-18 하나를 넣고 큐가 빈 것을 확인한 뒤 interrupt와 join하는 실제 흐름을 읽습니다.

PRINTER · 큐와 완료 경계

큐가 빈 시점과 출력이 끝난 시점은 다르다

isEmpty는 항목을 꺼냈다는 사실입니다. 이 예제는 콘솔 로그 뒤 다음 take에서 중단되고 join으로 회수됩니다.

큐에서 꺼낸 항목과 마지막 회수take뒤빈큐와처리중post-18을구별합니다. 원코드는콘솔로그뒤다음take에서취소를받고join으로회수하며main의interrupt와콘솔로그앞뒤는고정하지않습니다.
submit(post-18)큐에 한 항목 등록
연결 관계submit(post-18) · 큐에 한 항목 등록 → worker가 take · 큐 빈 상태 / 처리 중 항목
worker가 take큐 빈 상태 / 처리 중 항목
연결 관계worker가 take · 큐 빈 상태 / 처리 중 항목 → 콘솔 printed=post-18 · main 취소와 앞뒤는 가변
콘솔 printed=post-18main 취소와 앞뒤는 가변
연결 관계콘솔 printed=post-18 · main 취소와 앞뒤는 가변 → 다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신
다음 take의 중단interrupt 요청을중단 예외로 수신
연결 관계다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신 → main의 join 정상 반환 · printer-cancelled · pending=0
main의 join 정상 반환printer-cancelledpending=0

실선: 항목 처리와 마지막 종료의 관계isEmpty는 출력 완료 신호 아님interrupt즉시 종료를 뜻하지 않음

큐에서 꺼낸 항목과 마지막 회수take뒤빈큐와처리중post-18을구별합니다. 원코드는콘솔로그뒤다음take에서취소를받고join으로회수하며main의interrupt와콘솔로그앞뒤는고정하지않습니다.
submit(post-18)큐에 한 항목 등록
연결 관계submit(post-18) · 큐에 한 항목 등록 → worker가 take · 큐 빈 상태 / 처리 중 항목
worker가 take큐 빈 상태 / 처리 중 항목
연결 관계worker가 take · 큐 빈 상태 / 처리 중 항목 → 콘솔 printed=post-18 · main 취소와 앞뒤는 가변
콘솔 printed=post-18main 취소와 앞뒤는 가변
연결 관계콘솔 printed=post-18 · main 취소와 앞뒤는 가변 → 다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신
다음 take의 중단interrupt 요청을중단 예외로 수신
연결 관계다음 take의 중단 · interrupt 요청을 · 중단 예외로 수신 → main의 join 정상 반환 · printer-cancelled · pending=0
main의 join 정상 반환printer-cancelledpending=0

실선: 항목 처리와 마지막종료의 관계isEmpty는 출력 완료 신호아님interrupt즉시 종료를 뜻하지않음

  1. submit("post-18")

    확인된 사실
    항목 하나를 큐에 넣음
    아직 뜻하지 않는 것
    출력 완료
  2. jobs.isEmpty() 확인

    확인된 사실
    소비자가 항목을 꺼냄
    아직 뜻하지 않는 것
    콘솔 출력 완료
  3. interrupt() 요청

    확인된 사실
    다음 중단 가능한 대기에 취소를 전달
    아직 뜻하지 않는 것
    요청 즉시 작업자 종료
  4. join() 반환

    확인된 사실
    작업자 종료까지 회수
    아직 뜻하지 않는 것
    새 submit을 거부하는 수용 정책

이 원문은 printed=post-18 다음 printer-cancelled pending=0을 남깁니다. 꺼낸 항목의 콘솔 로그 뒤 다음 take가 중단됩니다. 큐 확인 반복에는 자체 시간 제한이 없고, drain·running 플래그·Outcome은 이 클래스에 없습니다.

이 예의 조건

  • main의 interrupt와 콘솔 출력의 앞뒤는 고정되지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2take 뒤 빈 큐와 처리 중 post-18을 구별합니다. 원래 코드는 콘솔 로그 뒤 다음 take에서 취소를 받고 join으로 회수하며 main의 interrupt와 콘솔 로그 앞뒤는 고정하지 않습니다.
6큐 빈 상태
7처리 중 항목
18interrupt 즉시 종료를 뜻하지 않음
원 도표의 설명과 주석
구분내용
설명take 뒤 빈 큐와 처리 중 post-18을 구별합니다. 원래 코드는 콘솔 로그 뒤 다음 take에서 취소를 받고 join으로 회수하며 main의 interrupt와 콘솔 로그 앞뒤는 고정하지 않습니다.
전체 읽기 정보큐가 빈 시점과 출력이 끝난 시점은 다르다 큐가 빈 시점과 출력이 끝난 시점은 다르다 InterruptiblePrinter의 post-18 하나를 넣고 큐가 빈 것을 확인한 뒤 interrupt와 join하는 실제 흐름을 읽습니다. PRINTER · 큐와 완료 경계 큐가 빈 시점과 출력이 끝난 시점은 다르다 isEmpty는 항목을 꺼냈다는 사실입니다. 이 예제는 콘솔 로그 뒤 다음 take에서 중단되고 join으로 회수됩니다. submit("post-18") 확인된 사실 항목 하나를 큐에 넣음 아직 뜻하지 않는 것 출력 완료 jobs.isEmpty() 확인 확인된 사실 소비자가 항목을 꺼냄 아직 뜻하지 않는 것 콘솔 출력 완료 interrupt() 요청 확인된 사실 다음 중단 가능한 대기에 취소를 전달 아직 뜻하지 않는 것 요청 즉시 작업자 종료 join() 반환 확인된 사실 작업자 종료까지 회수 아직 뜻하지 않는 것 새 submit을 거부하는 수용 정책 이 원문은 printed=post-18 다음 printer-cancelled pending=0 을 남깁니다. 꺼낸 항목의 콘솔 로그 뒤 다음 take가 중단됩니다. 큐 확인 반복에는 자체 시간 제한이 없고, drain·running 플래그·Outcome은 이 클래스에 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
항목 등록

submit(post-18)이 항목을 큐에 넣습니다.

  • 큐: post-18
  • 소유: 아직 큐
  • worker의 take가 항목 소유를 가져갑니다.
처리 중

take 뒤 큐는 비어 있고 worker가 출력할 항목을 보유합니다.

  • 큐: 빈 큐
  • 처리: printed=post-18
  • isEmpty만으로 출력 완료를 판정할 수 없습니다.
취소 회수

다음 take가 중단 예외를 받고 worker가 종료합니다.

  • join 뒤: printer-cancelled
  • pending: 0
  • main의 정상 join이 종료를 회수합니다.

interrupt 순간 큐에 남은 작업을 폐기할지 저장할지 재처리할지는 기능 정책입니다.

원 post-18은 take가 반환한 뒤 큐 밖의 처리 중 항목입니다. main이 빈 큐를 확인하고 취소해도 작업자는 이 항목의 콘솔 로그를 출력한 뒤 다음 중단 가능 take에서 예외를 받습니다. 큐가 비었다는 순간에는 그 로그가 아직 없을 수 있습니다. 실물 장치 출력까지 확인하려면 별도의 완료 응답이 필요하며, 이 예제의 println은 장치 완료 증거가 아닙니다.


interrupt와 정상 완료 구분

취소는 사용자 요청, 기한 만료, 프로세스 shutdown 등 원인이 다를 수 있습니다.

플래그에는 요청 횟수나 이유가 들어 있지 않습니다. 새 소거 없이 interrupt를 두 번 요청해도 두 개의 항목이 쌓이는 것이 아니라 중단 상태가 true로 표시됩니다. 사용자 취소인지 기한 만료인지, 어떤 입력을 회수할지는 소유자의 별도 상태·결과 기록으로 표현해야 합니다.

모든 경우를 false로 반환하면 재시도 여부를 정할 수 없습니다.

완료·취소·실패를 별도 결과로 표현하고 예외 원인을 보존합니다.

interrupt를 업무 오류로 로깅할 때 스택 추적을 매번 오류 수준으로 남기면 정상 shutdown이 장애처럼 보입니다.

요청된 shutdown인지 예상 밖 interrupt인지 소유자 상태와 비교합니다.

반대로 아무 로그도 없이 종료하면 누락 작업을 찾기 어렵습니다.

app/CancellableDocumentConsumer.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public final class CancellableDocumentConsumer {
    private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    private final List<String> completed = new ArrayList<>();
    private final Thread thread = new Thread(this::consume, "document-consumer");

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

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

    public void cancel() throws InterruptedException {
        thread.interrupt();
        thread.join();
    }

    private void consume() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                String value = queue.take();
                synchronized (completed) {
                    completed.add(value);
                }
                System.out.println("consumed=" + value);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public List<String> completed() {
        synchronized (completed) {
            return List.copyOf(completed);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CancellableDocumentConsumer c = new CancellableDocumentConsumer();
        c.start();
        c.add("thread");
        while (c.completed().isEmpty()) {
            Thread.onSpinWait();
        }
        c.cancel();
        System.out.println("snapshot=" + c.completed());
    }
}

완료 목록을 락 아래 갱신하고 스냅샷으로 반환합니다.

취소가 join까지 수행하므로 호출 이후에는 새 완료가 추가되지 않습니다.

큐에 남은 입력은 별도 보상 처리 대상으로 볼 수 있습니다.

작업자 종료와 입력 접수 종료도 구분하세요. 이 클래스의 cancel()은 작업자를 join하지만 add()는 이후에도 큐에 넣습니다. 종료 뒤 입력을 거부하는 기능은 아직 없습니다. 서비스로 확장한다면 접수 상태 확인과 등록을 같은 동기화 규칙 안에서 처리하고, 이미 접수한 입력의 폐기·저장·재처리 정책을 정해야 합니다. 단순히 접수 플래그를 한 번 읽고 나중에 add하면 종료와 등록이 경합할 수 있습니다.


중첩 메서드의 interrupt 책임

가장 안쪽 블로킹 메서드가 정책을 모두 결정하면 재사용이 어렵습니다.

가능하면 InterruptedException을 올려 소유자 반복문이 종료·재시도·무시 중 하나를 택하게 합니다.

Runnable.run처럼 검사 예외를 선언할 수 없는 최상위 접점에서 catch하고 최종 결과를 기록합니다.

InterruptedException을 RuntimeException으로 감쌀 때도 원래 원인과 플래그 정책을 문서화합니다.

실행기 Future는 interrupt 요청과 작업 결과를 별도 API로 표현하므로 직접 Thread보다 선택지가 선명합니다.


연습 문제

작업자가 20ms마다 heartbeat 번호를 올리며 sleep하게 만드세요.

main은 첫 heartbeat를 확인한 뒤 interrupt하고 join합니다.

작업자는 catch에서 플래그를 복원한 후 종료 이유를 기록해야 합니다.

정답과 경합 방지

래치로 첫 heartbeat 증가를 확인한 뒤 interrupt를 요청합니다.

heartbeat 증가 횟수는 스케줄러에 따라 달라 규칙에 넣지 않습니다.

exercise/InterruptibleHeartbeatSolution.java
import java.util.concurrent.CountDownLatch;

public final class InterruptibleHeartbeatSolution {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch first = new CountDownLatch(1);
        int[] beats = {0};
        Thread worker =
                new Thread(
                        () -> {
                            try {
                                while (true) {
                                    beats[0]++;
                                    first.countDown();
                                    Thread.sleep(20);
                                }
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                System.out.println(
                                        "reason=interrupt, flag="
                                                + Thread.currentThread().isInterrupted());
                            }
                        },
                        "heartbeat");
        worker.start();
        first.await();
        worker.interrupt();
        worker.join();
        System.out.println("terminated=" + !worker.isAlive() + ", beats=" + beats[0]);
    }
}

reason의 플래그는 true이고 terminated도 true입니다.

beats는 1 이상이라는 조건만 안정적입니다.

interrupt를 올바르게 쓰려면 플래그를 지우는 연산, 블로킹 예외 경로, 복원할 계층, 남은 업무 정책을 함께 정해야 합니다.

단순히 interrupt() 한 줄을 호출하는 것으로 shutdown이 완성되지 않습니다.


중단 신호와 남은 작업 판단하기

각 상황은 독립적이며 명시한 요청 외에 새로운 interrupt는 없다고 가정합니다.

  1. 현재 스레드가 자기 자신에게 interrupt를 두 번 요청합니다. 이어 isInterrupted() 한 번, Thread.interrupted() 두 번을 차례로 호출합니다. 세 반환 값은 무엇이며 두 요청의 횟수를 여기서 복원할 수 있나요?
  2. main의 플래그는 true입니다. 다른 Thread 객체인 worker를 참조하여 main에서 worker.interrupted()를 호출합니다. 누구의 상태를 읽고 지우나요? worker 상태를 지우지 않고 조회하는 올바른 호출은 무엇인가요?
  3. 한 항목을 take한 소비자가 출력 전에 게이트에서 기다리고 있어 큐는 비었지만 출력은 아직 끝나지 않았습니다. 빈 큐만 보고 완료라고 판정해도 되나요? 이후 소비자를 취소하고 join한 뒤, 본문의 CancellableDocumentConsumer에 add를 다시 호출하면 입력이 자동 거부되나요?
플래그와 입력 수용의 해설
  1. true, true, false입니다. isInterrupted는 그대로 두고 첫 interrupted가 요청 상태를 소비합니다. 플래그는 요청 횟수나 이유를 저장하지 않으므로 두 번이라는 횟수를 복원할 수 없습니다.
  2. 정적 interrupted의 대상은 호출 중인 main이므로 main의 true를 반환하고 지웁니다. worker를 조회하려면 worker.isInterrupted()를 사용합니다. 변수 이름이 호출 대상을 바꾸지 않습니다.
  3. 완료라고 판정할 수 없습니다. 큐 밖의 처리 중 항목이 있으므로 별도 완료 응답이나 종료 회수 경계가 필요합니다. 본문의 add에는 종료 상태 검사가 없어 큐에 들어가지만 종료한 소비자가 처리하지 않습니다. 취소·join 외에 접수 종료와 남은 입력 처리 정책을 정해야 합니다.