Future 취소·시간 초과·실패
Future 취소와 인터럽트의 협력 관계를 이해하고 시간 제한·실행 실패·취소 예외를 구분합니다.
Future는 진행 중이거나 정상·예외·취소로 완료될 수 있습니다.
정상·예외·취소 완료는 모두 isDone이 true인 상태입니다. TimeoutException은 Future의 완료 상태가 아니라 get 호출자의 대기 시간이 만료됐다는 뜻입니다.
이런 결과를 하나의 null이나 일반 오류로 합치면 재시도와 사용자 메시지를 올바르게 선택할 수 없습니다.
cancel(true)는 실행 중 스레드에 인터럽트를 요청할 수 있지만 코드를 강제로 죽이지 않습니다.
작업이 인터럽트 상태를 무시하면 계속 실행될 수 있습니다.
취소는 호출자와 작업자가 함께 지키는 협력 규칙입니다.
Future의 완료 상태와 작업 스레드의 생존 상태는 별도입니다. cancel(false)의 false는 실행 중 스레드에 인터럽트를 요청하지 않는다는 뜻입니다. 취소가 성공한 FutureTask는 isDone()·isCancelled()가 참이고 get()이 CancellationException을 던져도, 이미 실행 중인 본문은 계속될 수 있습니다. 취소 성공을 확인한 뒤 실제 종료까지 필요하다면 그 종료를 관찰하는 별도 수단을 사용해야 합니다.
인터럽트를 삼켜 취소 뒤에도 계속되는 작업
import java.util.concurrent.Executors;
public final class UncooperativeCancellation {
public static void main(String[] args) throws Exception {
var executor = Executors.newSingleThreadExecutor();
var future = executor.submit(() -> {
for (int i = 0; i < 5; i++) {
try { Thread.sleep(50); }
catch (InterruptedException ignored) { }
System.out.println("step=" + i);
}
});
Thread.sleep(20);
System.out.println("cancelled=" + future.cancel(true));
executor.shutdown();
executor.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS);
}
}취소 결과와 람다 본문의 실행 여부를 나누어 읽는다
본문 진입 전 취소/진입 뒤 취소/완료 뒤 취소의 Future 상태와 worker의 계속 출력이 다릅니다. 두 수명을 분리해 가능 경로별 cancel 결과와 본문 상태를 따라갑니다.
UncooperativeCancellation의 FutureTask 취소와 원문 람다의 실행을 세 경우로 나누며, 취소가 작업 본문의 완료를 기다리지 않는다는 점을 보여줍니다.
취소 경합 · Future와 작업 본문
취소 결과와 람다 본문의 실행 여부를 나누어 읽는다
cancelled=true여도 인터럽트를 무시하는 실행 본문은 끝까지 진행할 수 있습니다.
취소 판정과 작업 본문의 수명
cancel(true)의 세 시점 비교다. 진입 전 취소와 실행 중 취소가 모두 true일 수 있지만, 인터럽트를 무시하는 실행 중 본문은 step 0부터 4까지 계속된다. 완료가 먼저면 false다. 진입 전·완료 후 취소는 미관측 조건이며 sleep(20)은 본문 진입을 보장하지 않는다.
상태별 상세 해설
본문 진입을 막은 취소
- main의 cancel(true) 결과
cancelled=true- 람다의 step 출력
- step 출력 없음
본문이 실행된 상태에서 취소 성공
- main의 cancel(true) 결과
cancelled=true- 람다의 step 출력
- step=0부터 step=4까지 출력 · 인터럽트에 중단하는 분기 없음
정상 완료가 취소보다 앞섬
- main의 cancel(true) 결과
cancelled=false- 람다의 step 출력
- step=0부터 step=4 뒤 취소 결과 출력
이번 실행은 step=0, cancelled=true, step=1부터 step=4 순으로 출력했습니다. 다른 실행의 cancelled=true 줄은 step 줄 앞·사이·뒤에 올 수 있고, sleep(20)은 본문 시작을 보장하지 않습니다. 표의 시작 전 취소와 완료 후 취소는 미관측 경로입니다.
이 예의 조건
- 취소 시점 세 조건을 공통 판정·본문·관측 단계마다 비교합니다. sleep(20)은 본문 진입을 보장하지 않습니다.
- 시작 전·완료 후 취소는 미관측 조건입니다. 원 관측은 step=0, cancelled=true, step=1..4입니다.
- 본문은 InterruptedException을 무시합니다. awaitTermination(1초)의 반환값은 검사하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 설명 | |
| 전체 읽기 정보 | 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 UncooperativeCancellation의 FutureTask 취소와 원문 람다의 실행을 세 경우로 나누며, 취소가 작업 본문의 완료를 기다리지 않는다는 점을 보여줍니다. 취소 경합 · Future와 작업 본문 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 cancelled=true여도 인터럽트를 무시하는 실행 본문은 끝까지 진행할 수 있습니다. 본문 진입을 막은 취소 main의 cancel(true) 결과 cancelled=true 람다의 step 출력 step 출력 없음 본문이 실행된 상태에서 취소 성공 main의 cancel(true) 결과 cancelled=true 람다의 step 출력 step=0부터 step=4까지 출력 · 인터럽트에 중단하는 분기 없음 정상 완료가 취소보다 앞섬 main의 cancel(true) 결과 cancelled=false 람다의 step 출력 step=0부터 step=4 뒤 취소 결과 출력 이번 실행은 step=0 , cancelled=true , step=1부터 step=4 순으로 출력했습니다. 다른 실행의 cancelled=true 줄은 step 줄 앞·사이·뒤에 올 수 있고, sleep(20)은 본문 시작을 보장하지 않습니다. 표의 시작 전 취소와 완료 후 취소는 미관측 경로입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 취소 시점별 판정 본문 진입 전·진행 중·정상 완료 뒤 중 어느 시점이 앞섰는지에 따라 cancel 결과가 달라집니다. |
|
|
| 본문과 Future의 상태 취소된 Future와 실제 람다 실행 여부를 각 조건에서 따로 읽습니다. |
|
|
| 출력과 종료 확인의 범위 각 조건의 출력과 main의 종료 대기를 worker 완료 보장과 구분합니다. |
|
|
취소 결과와 람다 본문의 실행 여부를 나누어 읽는다
본문 진입 전 취소/진입 뒤 취소/완료 뒤 취소의 Future 상태와 worker의 계속 출력이 다릅니다. 두 수명을 분리해 가능 경로별 cancel 결과와 본문 상태를 따라갑니다.
UncooperativeCancellation의 FutureTask 취소와 원문 람다의 실행을 세 경우로 나누며, 취소가 작업 본문의 완료를 기다리지 않는다는 점을 보여줍니다.
취소 경합 · Future와 작업 본문
취소 결과와 람다 본문의 실행 여부를 나누어 읽는다
cancelled=true여도 인터럽트를 무시하는 실행 본문은 끝까지 진행할 수 있습니다.
취소 판정과 작업 본문의 수명
cancel(true)의 세 시점 비교다. 진입 전 취소와 실행 중 취소가 모두 true일 수 있지만, 인터럽트를 무시하는 실행 중 본문은 step 0부터 4까지 계속된다. 완료가 먼저면 false다. 진입 전·완료 후 취소는 미관측 조건이며 sleep(20)은 본문 진입을 보장하지 않는다.
상태별 상세 해설
본문 진입을 막은 취소
- main의 cancel(true) 결과
cancelled=true- 람다의 step 출력
- step 출력 없음
본문이 실행된 상태에서 취소 성공
- main의 cancel(true) 결과
cancelled=true- 람다의 step 출력
- step=0부터 step=4까지 출력 · 인터럽트에 중단하는 분기 없음
정상 완료가 취소보다 앞섬
- main의 cancel(true) 결과
cancelled=false- 람다의 step 출력
- step=0부터 step=4 뒤 취소 결과 출력
이번 실행은 step=0, cancelled=true, step=1부터 step=4 순으로 출력했습니다. 다른 실행의 cancelled=true 줄은 step 줄 앞·사이·뒤에 올 수 있고, sleep(20)은 본문 시작을 보장하지 않습니다. 표의 시작 전 취소와 완료 후 취소는 미관측 경로입니다.
이 예의 조건
- 취소 시점 세 조건을 공통 판정·본문·관측 단계마다 비교합니다. sleep(20)은 본문 진입을 보장하지 않습니다.
- 시작 전·완료 후 취소는 미관측 조건입니다. 원 관측은 step=0, cancelled=true, step=1..4입니다.
- 본문은 InterruptedException을 무시합니다. awaitTermination(1초)의 반환값은 검사하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 설명 | |
| 전체 읽기 정보 | 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 UncooperativeCancellation의 FutureTask 취소와 원문 람다의 실행을 세 경우로 나누며, 취소가 작업 본문의 완료를 기다리지 않는다는 점을 보여줍니다. 취소 경합 · Future와 작업 본문 취소 결과와 람다 본문의 실행 여부를 나누어 읽는다 cancelled=true여도 인터럽트를 무시하는 실행 본문은 끝까지 진행할 수 있습니다. 본문 진입을 막은 취소 main의 cancel(true) 결과 cancelled=true 람다의 step 출력 step 출력 없음 본문이 실행된 상태에서 취소 성공 main의 cancel(true) 결과 cancelled=true 람다의 step 출력 step=0부터 step=4까지 출력 · 인터럽트에 중단하는 분기 없음 정상 완료가 취소보다 앞섬 main의 cancel(true) 결과 cancelled=false 람다의 step 출력 step=0부터 step=4 뒤 취소 결과 출력 이번 실행은 step=0 , cancelled=true , step=1부터 step=4 순으로 출력했습니다. 다른 실행의 cancelled=true 줄은 step 줄 앞·사이·뒤에 올 수 있고, sleep(20)은 본문 시작을 보장하지 않습니다. 표의 시작 전 취소와 완료 후 취소는 미관측 경로입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 취소 시점별 판정 본문 진입 전·진행 중·정상 완료 뒤 중 어느 시점이 앞섰는지에 따라 cancel 결과가 달라집니다. |
|
|
| 본문과 Future의 상태 취소된 Future와 실제 람다 실행 여부를 각 조건에서 따로 읽습니다. |
|
|
| 출력과 종료 확인의 범위 각 조건의 출력과 main의 종료 대기를 worker 완료 보장과 구분합니다. |
|
|
awaitTermination(1초)의 반환값은 검사하지 않습니다. false를 반환했다면 main이 끝나도 작업자가 아직 실행 중일 수 있습니다. 이 실행기의 기본 작업자는 non-daemon이므로 정상 프로세스 종료와 main 반환도 구분해야 합니다.
인터럽트에 협력하는 검색 작업
import java.time.Duration;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class CooperativeSearch {
static int search(int target) throws InterruptedException {
for (int candidate = 0; candidate < Integer.MAX_VALUE; candidate++) {
if ((candidate & 0x3fff) == 0 && Thread.interrupted()) {
throw new InterruptedException("search cancelled");
}
if (candidate == target) return candidate;
}
return -1;
}
public static void main(String[] args) throws Exception {
var executor = Executors.newSingleThreadExecutor();
var future = executor.submit(() -> search(Integer.MAX_VALUE - 1));
try {
System.out.println(future.get(Duration.ofMillis(5).toNanos(), TimeUnit.NANOSECONDS));
} catch (TimeoutException e) {
future.cancel(true);
try { future.get(); }
catch (CancellationException cancelled) { System.out.println("cancelled"); }
} finally {
executor.shutdownNow();
}
}
}시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다
첫 get→timeout 뒤 cancel→두 번째 get에서 취소 성공/완료 경합 결과가 갈립니다. 각 호출의 반환과 출력 여부를 추적하되 관측한 cancelled를 유일한 결과로 만들지 않습니다.
CooperativeSearch.main의 첫 timed get과 timeout 이후 cancel 및 두 번째 get이 만드는 세 출력 경로를 비교합니다.
검색 대기 · 완료와 취소의 세 경로
시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다
시간 초과 뒤 정상 완료가 취소보다 앞서면, 두 번째 get의 값은 출력하지 않습니다.
첫 get과 취소 경합의 세 출력
첫 timed get이 정상 반환한 때만 숫자를 출력한다. timeout 뒤 취소 true는 cancelled를 출력하며, 완료 선행 false는 두 번째 get의 값을 버린다. 첫 정상값은 2147483646이다. 원 관측은 cancelled이고 정상 숫자·무출력은 미관측 가능 경로다. shutdownNow 요청은 worker 완료 확인이 아니다.
상태별 상세 해설
첫 get이 정상 반환
- 후속 처리
- 그 값을 바로 println
- main이 출력하는 값
2147483646
시간 초과 뒤 취소 성공
- 후속 처리
- 두 번째 get의 CancellationException을 catch
- main이 출력하는 값
cancelled
시간 초과 뒤 정상 완료가 취소에 앞섬
- 후속 처리
- cancel은 false · 두 번째 get의 정상값을 버림
- main이 출력하는 값
- 출력 없음 · 정상 종료 가능
이번 실행은 cancelled를 출력했습니다. 표의 정상값 출력과 무출력 정상 종료는 미관측 가능 경로입니다. 원문은 cancel 반환값을 검사하지 않으며, finally의 shutdownNow도 작업자 종료를 기다리지는 않습니다.
이 예의 조건
- 첫 timed get 정상·timeout 뒤 취소 성공·timeout 뒤 정상 완료 선행의 세 조건을 공통 단계마다 비교합니다.
- 원 관측은 cancelled이며 정상값 출력·무출력 정상 종료는 미관측 가능 경로입니다.
- cancel 반환값은 원 main이 검사하지 않습니다. 5ms 예산과 shutdownNow 요청은 worker 종료 상한·완료 확인이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 설명 | |
| 전체 읽기 정보 | 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 CooperativeSearch.main의 첫 timed get과 timeout 이후 cancel 및 두 번째 get이 만드는 세 출력 경로를 비교합니다. 검색 대기 · 완료와 취소의 세 경로 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 시간 초과 뒤 정상 완료가 취소보다 앞서면, 두 번째 get의 값은 출력하지 않습니다. 첫 get이 정상 반환 후속 처리 그 값을 바로 println main이 출력하는 값 2147483646 시간 초과 뒤 취소 성공 후속 처리 두 번째 get의 CancellationException을 catch main이 출력하는 값 cancelled 시간 초과 뒤 정상 완료가 취소에 앞섬 후속 처리 cancel은 false · 두 번째 get의 정상값을 버림 main이 출력하는 값 출력 없음 · 정상 종료 가능 이번 실행은 cancelled 를 출력했습니다. 표의 정상값 출력과 무출력 정상 종료는 미관측 가능 경로입니다. 원문은 cancel 반환값을 검사하지 않으며, finally의 shutdownNow도 작업자 종료를 기다리지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 get의 결과 판정 5ms timed get이 정상 반환하거나 timeout이 되어 후속 처리를 선택합니다. |
|
|
| 후속 조치와 두 번째 get timeout 뒤 cancel과 완료가 경합하여 두 번째 get의 결과가 달라집니다. |
|
|
| 각 출력과 정리 요청 각 경로가 원 코드의 출력 규칙을 따르고 finally에서 shutdownNow를 요청합니다. |
|
|
시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다
첫 get→timeout 뒤 cancel→두 번째 get에서 취소 성공/완료 경합 결과가 갈립니다. 각 호출의 반환과 출력 여부를 추적하되 관측한 cancelled를 유일한 결과로 만들지 않습니다.
CooperativeSearch.main의 첫 timed get과 timeout 이후 cancel 및 두 번째 get이 만드는 세 출력 경로를 비교합니다.
검색 대기 · 완료와 취소의 세 경로
시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다
시간 초과 뒤 정상 완료가 취소보다 앞서면, 두 번째 get의 값은 출력하지 않습니다.
첫 get과 취소 경합의 세 출력
첫 timed get이 정상 반환한 때만 숫자를 출력한다. timeout 뒤 취소 true는 cancelled를 출력하며, 완료 선행 false는 두 번째 get의 값을 버린다. 첫 정상값은 2147483646이다. 원 관측은 cancelled이고 정상 숫자·무출력은 미관측 가능 경로다. shutdownNow 요청은 worker 완료 확인이 아니다.
상태별 상세 해설
첫 get이 정상 반환
- 후속 처리
- 그 값을 바로 println
- main이 출력하는 값
2147483646
시간 초과 뒤 취소 성공
- 후속 처리
- 두 번째 get의 CancellationException을 catch
- main이 출력하는 값
cancelled
시간 초과 뒤 정상 완료가 취소에 앞섬
- 후속 처리
- cancel은 false · 두 번째 get의 정상값을 버림
- main이 출력하는 값
- 출력 없음 · 정상 종료 가능
이번 실행은 cancelled를 출력했습니다. 표의 정상값 출력과 무출력 정상 종료는 미관측 가능 경로입니다. 원문은 cancel 반환값을 검사하지 않으며, finally의 shutdownNow도 작업자 종료를 기다리지는 않습니다.
이 예의 조건
- 첫 timed get 정상·timeout 뒤 취소 성공·timeout 뒤 정상 완료 선행의 세 조건을 공통 단계마다 비교합니다.
- 원 관측은 cancelled이며 정상값 출력·무출력 정상 종료는 미관측 가능 경로입니다.
- cancel 반환값은 원 main이 검사하지 않습니다. 5ms 예산과 shutdownNow 요청은 worker 종료 상한·완료 확인이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 설명 | |
| 전체 읽기 정보 | 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 CooperativeSearch.main의 첫 timed get과 timeout 이후 cancel 및 두 번째 get이 만드는 세 출력 경로를 비교합니다. 검색 대기 · 완료와 취소의 세 경로 시간 초과 뒤 취소가 완료와 경합하면 출력도 달라진다 시간 초과 뒤 정상 완료가 취소보다 앞서면, 두 번째 get의 값은 출력하지 않습니다. 첫 get이 정상 반환 후속 처리 그 값을 바로 println main이 출력하는 값 2147483646 시간 초과 뒤 취소 성공 후속 처리 두 번째 get의 CancellationException을 catch main이 출력하는 값 cancelled 시간 초과 뒤 정상 완료가 취소에 앞섬 후속 처리 cancel은 false · 두 번째 get의 정상값을 버림 main이 출력하는 값 출력 없음 · 정상 종료 가능 이번 실행은 cancelled 를 출력했습니다. 표의 정상값 출력과 무출력 정상 종료는 미관측 가능 경로입니다. 원문은 cancel 반환값을 검사하지 않으며, finally의 shutdownNow도 작업자 종료를 기다리지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 get의 결과 판정 5ms timed get이 정상 반환하거나 timeout이 되어 후속 처리를 선택합니다. |
|
|
| 후속 조치와 두 번째 get timeout 뒤 cancel과 완료가 경합하여 두 번째 get의 결과가 달라집니다. |
|
|
| 각 출력과 정리 요청 각 경로가 원 코드의 출력 규칙을 따르고 finally에서 shutdownNow를 요청합니다. |
|
|
search는 candidate가 16,384의 배수일 때 Thread.interrupted()로 인터럽트 상태를 읽고 지웁니다. 취소를 확인하면 InterruptedException을 던지지만, 5ms 대기 예산이 작업자 종료까지의 시간 상한은 아닙니다.
CPU 루프는 적절한 간격으로 검사해 오버헤드와 취소 지연의 균형을 맞춥니다.
실패 원인의 도메인 결과 분류
import java.time.Duration;
import java.util.concurrent.*;
public final class FutureOutcomeClassifier {
sealed interface Outcome<T> permits Success, Failed, TimedOut, Cancelled {}
record Success<T>(T value) implements Outcome<T> {}
record Failed<T>(Throwable cause) implements Outcome<T> {}
record TimedOut<T>() implements Outcome<T> {}
record Cancelled<T>() implements Outcome<T> {}
static <T> Outcome<T> await(Future<T> future, Duration timeout) throws InterruptedException {
try {
return new Success<>(future.get(timeout.toNanos(), TimeUnit.NANOSECONDS));
} catch (TimeoutException e) {
return new TimedOut<>();
} catch (CancellationException e) {
return new Cancelled<>();
} catch (ExecutionException e) {
return new Failed<>(e.getCause());
}
}
public static void main(String[] args) throws Exception {
var executor = Executors.newSingleThreadExecutor();
try {
Future<Integer> failed = executor.submit(() -> {
throw new IllegalStateException("boom");
});
System.out.println(await(failed, Duration.ofSeconds(1)));
} finally {
executor.shutdownNow();
}
}
}FutureOutcomeClassifier.await가 get의 정상값과 네 예외를 처리하는 방법을 비교하며 Future의 완료 상태와 호출자의 대기 사건을 구분합니다.
대기 결과 · Future 상태와 호출자 사건
get의 반환·예외를 도메인 결과와 상위 전파로 나눈다
시간 초과와 호출자 인터럽트는 이번 대기의 사건이며 자동 취소를 뜻하지 않습니다.
| get에서 관찰한 결과 | await의 처리 | Future·작업에 대한 의미 |
|---|---|---|
| 정상 반환 value | Success(value) 반환 | Future 정상 완료 |
ExecutionException | Failed(e.getCause()) 반환 | Future 예외 완료 · 원인 객체 보존 |
CancellationException | Cancelled() 반환 | Future 취소 완료 · 작업자 종료와 별개 |
TimeoutException | TimedOut() 반환 | 이번 대기만 만료 · 자동 취소 안 함 |
InterruptedException | catch하지 않고 상위로 전파 | 대기 호출자의 인터럽트 · 자동 취소 안 함 |
- 정상 반환 value
- await의 처리
Success(value)반환- Future·작업에 대한 의미
- Future 정상 완료
ExecutionException- await의 처리
Failed(e.getCause())반환- Future·작업에 대한 의미
- Future 예외 완료 · 원인 객체 보존
CancellationException- await의 처리
Cancelled()반환- Future·작업에 대한 의미
- Future 취소 완료 · 작업자 종료와 별개
TimeoutException- await의 처리
TimedOut()반환- Future·작업에 대한 의미
- 이번 대기만 만료 · 자동 취소 안 함
InterruptedException- await의 처리
- catch하지 않고 상위로 전파
- Future·작업에 대한 의미
- 대기 호출자의 인터럽트 · 자동 취소 안 함
이번 main은 Failed 결과에 IllegalStateException: boom을 담아 출력했습니다. 1초 대기가 먼저 만료되면 TimedOut 경로도 가능합니다. 그 경로와 나머지 행은 이번에 관측하지 않은 await 코드·API의 처리 범위입니다.
누가 인터럽트됐는지도 구분하세요. 작업 내부에서 던진 InterruptedException으로 Future가 예외 완료되면 get()은 ExecutionException으로 감싸므로 Failed의 원인으로 분류됩니다. 반면 결과를 기다리는 호출자가 인터럽트되면 get() 자체가 InterruptedException을 던집니다. await는 이를 일반 실패나 시간 초과로 바꾸지 않고 상위 취소 정책에 넘깁니다. 이 예제에 전달할 대기 예산은 양수이며 Duration.toNanos()로 표현 가능한 범위라고 가정합니다. 이 메서드가 null·과도하게 큰 Duration까지 검증하는 공개 API라고 확대해서 해석하지 않습니다.
취소와 외부 효과의 복구
이미 전송한 외부 요청이나 기록한 변경은 Future를 취소한다고 되돌아가지 않습니다. 그런 효과가 있는 작업에서는 부분 완료와 보상 기준을 따로 정합니다. 이 장의 검색·해시 원문에는 업무 데이터를 저장하는 단계가 없습니다.
연습 문제
바이트 배열을 여러 번 해시하는 Callable을 만들고 get의 20밀리초 대기 시간이 초과되면 취소를 요청하세요.
반복 사이 인터럽트를 확인하고 최종 상태를 문자열로 출력합니다.
정답과 해설
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class TimedHashCancellationSolution {
static String hash(byte[] input) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] value = input;
for (int i = 0; i < 1_000_000; i++) {
if (Thread.interrupted()) throw new InterruptedException();
value = digest.digest(value);
}
return HexFormat.of().formatHex(value);
}
public static void main(String[] args) throws Exception {
var executor = Executors.newSingleThreadExecutor();
var future = executor.submit(() -> hash("java".getBytes(java.nio.charset.StandardCharsets.UTF_8)));
try {
System.out.println(future.get(20, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
future.cancel(true);
System.out.println("timed-out-and-cancelled=" + future.isCancelled());
} finally {
executor.shutdownNow();
}
}
}이번 출력은 timed-out-and-cancelled=true였습니다. 첫 get이 정상 완료하면 해시 문자열을 출력합니다. 시간이 초과되면 취소를 시도한 뒤 true 또는 false를 출력할 수 있으며, 완료가 취소보다 먼저 확정되면 false가 됩니다. 정상 해시와 취소 false 경로는 이번 실행에서 관측하지 않았습니다.
hash는 각 digest 호출 전에 Thread.interrupted()를 확인합니다. 한 번의 digest 계산 도중에 즉시 중단하는 것은 아니며, 취소 요청 뒤의 실제 종료를 기다리는 코드는 main에 없습니다.
Future 중단 규칙의 최종 점검
시간 초과는 관찰자가 기다리기를 멈춘 사건이고 취소는 작업 중단 요청입니다.
둘을 연결할지 호출자가 결정합니다.
작업자는 인터럽트에 협력하고, 실패 원인은 ExecutionException의 원인에서 잃지 않아야 합니다.
연습: 취소 결과·작업자 종료·호출자 인터럽트를 따로 판정하기
조건 1. Future는 이미 42로 정상 완료됐습니다. 같은 호출자가 cancel(true)를 한 번 호출한 뒤 FutureOutcomeClassifier.await에 1초 예산을 전달합니다. 취소 반환값·isCancelled·결과 분류를 구하세요.
풀이. 완료 뒤 취소는 실패하므로 반환값은 false, isCancelled()도 false이며 결과는 Success(42)입니다. 취소 요청보다 완료가 먼저 확정된 경로입니다. 취소 반환값 false만 보고 일반적인 모든 경합에서 미취소라고 단정할 수는 없지만, 여기서는 다른 취소 호출자가 없고 정상 완료가 먼저라는 조건으로 결론을 고정했습니다.
조건 2. FutureTask 본문이 시작됐음을 latch로 확인한 뒤, 본문을 별도 release latch에서 기다리게 합니다. 이 상태에서 cancel(false)가 true를 반환했으며 아직 release하지 않았습니다. isDone, isCancelled, get의 결과와 본문 종료 여부를 구하세요.
풀이. Future의 두 상태 값은 모두 true이고 get()은 CancellationException을 던집니다. 그러나 본문은 아직 latch에서 기다리므로 종료하지 않았습니다. false 인자는 인터럽트를 요청하지 않기 때문에 release 뒤 본문의 남은 외부 효과도 실행될 수 있습니다. Future의 취소 완료가 이미 시작한 동작을 되돌리는 것은 아닙니다.
조건 3. 아직 완료되지 않은 Future를 기다리는 호출자가 인터럽트되고 await의 get에서 InterruptedException이 발생합니다. 분류기는 Failed, TimedOut, Cancelled 중 무엇을 반환할까요? 작업 내부에서 같은 예외가 발생해 예외 완료된 경우도 비교하세요.
풀이. 첫 경우는 셋 중 어느 결과도 반환하지 않고 InterruptedException을 호출자에게 전파합니다. Future가 자동으로 취소됐다는 뜻도 아닙니다. 상위 호출자는 자신의 취소 정책에 따라 전파하거나 적절한 경계에서 인터럽트 상태를 복구합니다. 두 번째 경우에는 get()이 ExecutionException을 던지므로 Failed 안에 원래 작업 예외가 보존됩니다.