线程池把“提交任务”和“执行任务”分成了两个阶段。调用处的 try/catch 能处理提交时的错误,却不能跨线程捕获任务后来抛出的所有异常。任务失败应在哪里观察,取决于提交方式和具体执行器。
execute:给工作线程配置未捕获异常处理器
下面使用 Java 8 的固定线程池作最小演示。通过 ThreadFactory 创建线程,再给线程设置处理器,而不是在线程池对象上调用不存在的同名方法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class ExecuteFailureDemo {
public static void main(String[] args) {
ThreadFactory factory = task -> {
Thread worker = Executors.defaultThreadFactory().newThread(task);
worker.setUncaughtExceptionHandler((thread, error) ->
System.err.println(thread.getName() + ": " + error));
return worker;
};
ExecutorService pool = Executors.newFixedThreadPool(1, factory);
try {
pool.execute(() -> {
throw new IllegalStateException("task failed");
});
} finally {
pool.shutdown();
}
}
}
对于这里的 ThreadPoolExecutor,直接交给 execute 的 Runnable 如果让运行时异常逸出,会进入工作线程的未捕获异常处理路径。处理器负责记录或上报,不会把异常重新抛回原来的提交线程,也不代表失败任务被自动重试。
这个固定线程池仅用于说明机制;实际服务还应按业务设计有界队列、容量和拒绝策略。
submit:通过 Future 消费任务失败
submit 通常把任务包装为 FutureTask,任务异常保存在 Future 中。不能因为配置了工作线程处理器,就忽略返回值。
import java.util.concurrent.*;
public class SubmitFailureDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
try {
Future<Integer> result = pool.submit(() -> {
throw new IllegalStateException("task failed");
});
try {
result.get();
} catch (ExecutionException error) {
System.err.println("任务失败:" + error.getCause());
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
return;
}
} finally {
pool.shutdown();
}
}
}
get 还可能因取消而抛 CancellationException;带超时的 get 要另外处理 TimeoutException。需要重试时,先判断任务是否幂等,不要在每一层都无条件重试。
提交被拒绝是另一种失败
关闭后的线程池,或采用拒绝策略且容量不足的线程池,可能在提交处抛出 RejectedExecutionException。以默认抛异常策略为例:
try {
pool.execute(task); // pool 和 task 是业务中已有的对象
} catch (RejectedExecutionException error) {
// 在这里决定明确返回失败、限流或进入受控补偿流程
throw error;
}
自定义拒绝策略也可能采用调用者执行或丢弃,不能只靠一个 catch 判断所有任务都被接收。
ThreadPoolExecutor 的 afterExecute 可作为统一观测点,但 FutureTask 保存的异常不一定出现在它的 Throwable 参数中,需要按 Future 状态取结果判断。Java 8 ThreadPoolExecutor
任务边界记录上下文,结果消费处决定业务处理,统一观测负责兜底。明确各层职责,比把同一条堆栈重复记录三次更有用。