Future 취소·시간 초과·실패
취소가 강제 종료라는 오해를 깨고, 인터럽트 협력 작업·시간 제한 `get`·ExecutionException·CancellationException을 호출 접점에서 분류합니다.
Future의 미완료에는 여러 이유가 있습니다.
계산 중일 수 있고, 시간 예산을 넘겼을 수 있으며, 취소되었거나 예외로 끝났을 수 있습니다.
이 상태를 하나의 null이나 일반 오류로 합치면 재시도와 사용자 메시지를 올바르게 선택할 수 없습니다.
cancel(true)는 실행 중 스레드에 인터럽트를 요청할 수 있지만 코드를 강제로 죽이지 않습니다.
작업이 인터럽트 상태를 무시하면 계속 실행될 수 있습니다.
취소는 호출자와 작업자가 함께 지키는 협력 규칙입니다.
인터럽트를 삼켜 취소 뒤에도 계속되는 작업
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는 취소 상태지만 작업은 인터럽트를 버리고 남은 단계를 실행합니다.
취소 결과와 실제 부수 효과 중단이 어긋납니다.
결과 상태별 처리
- 정상 완료는
get의 반환값으로 전달된다. - 작업 예외는 ExecutionException의 원인으로 보존된다.
- 취소된
Future의get은 CancellationException을 던진다. - 시간 제한
get의 예산 초과는 TimeoutException이며 작업은 자동 취소되지 않는다. - 대기 호출자의 인터럽트는 InterruptedException으로 나타난다.
- 취소 작업은 블로킹 API의 인터럽트를 전파하거나 반복에서 상태를 확인한다.
인터럽트에 협력하는 검색 작업
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();
}
}
}시간 초과 후 명시적으로 취소하고 작업은 인터럽트 검사를 통해 빠져나옵니다. 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();
}
}
}호출자 인터럽트는 결과로 바꾸지 않고 상위 취소로 전파합니다.
업무상 분류 가능한 Future 상태만 sealed 결과로 변환합니다.
취소·시간 제한·실패 결과 결정
| 사건 | 호출자 조치 | 작업자 책임 |
|---|---|---|
| 시간 초과 | 계속·취소 결정 | 자동 중단 아님 |
취소 true | get 중단 처리 | 인터럽트 협력 |
| 실행 예외 | 원인 분류 | 원인 보존 |
| 호출자 인터럽트 | 상위 전파 | 정리 후 종료 |
연습 문제
바이트 배열을 여러 번 해시하는 Callable을 만들고 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();
}
}
}빠른 환경에서 완료될 가능성도 있으므로 테스트는 두 정상 경로를 허용합니다. 취소를 결정했다면 작업이 인터럽트를 관찰하도록 구현한 것이 핵심입니다.
Future 중단 규칙의 최종 점검
시간 초과는 관찰자가 기다리기를 멈춘 사건이고 취소는 작업 중단 요청입니다. 둘을 연결할지 호출자가 결정합니다. 작업자는 인터럽트에 협력하고, 실패 원인은 ExecutionException의 원인에서 잃지 않아야 합니다.