问题
如何实现主线程捕获子线程异常?
答案
核心思路
由于异常无法跨线程传播,需要通过中间机制将子线程的异常传递给主线程。常见方案:
- 在子线程内部捕获异常,通过共享变量传递
- 使用UncaughtExceptionHandler捕获未捕获异常
- 使用Future.get()获取异常
- 使用CompletableFuture的异常处理机制
- 使用线程池的afterExecute钩子
方案一:共享变量传递异常
原理:子线程捕获异常后存储到共享变量,主线程读取。
public class SharedVariableDemo {
// 使用volatile保证可见性
private static volatile Exception exception = null;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
int result = 10 / 0;
} catch (Exception e) {
exception = e; // 存储异常
}
});
thread.start();
thread.join(); // 等待子线程结束
// 主线程检查异常
if (exception != null) {
System.out.println("捕获到子线程异常: " + exception.getMessage());
exception.printStackTrace();
}
}
}
优点:
- 简单直接,容易理解
缺点:
- 需要手动管理共享变量,容易出错
- 多线程场景下需要使用ConcurrentHashMap存储每个线程的异常
- 必须调用join()等待线程结束
方案二:UncaughtExceptionHandler(推荐)
原理:为线程设置异常处理器,当线程抛出未捕获异常时自动回调。
2.1 线程实例级别的Handler
public class UncaughtExceptionHandlerDemo {
public static void main(String[] args) throws InterruptedException {
// 用于存储异常的容器
final Exception[] exceptionHolder = new Exception[1];
final CountDownLatch latch = new CountDownLatch(1);
Thread thread = new Thread(() -> {
int result = 10 / 0; // 抛出异常
});
// 设置异常处理器
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("线程 " + t.getName() + " 抛出异常");
exceptionHolder[0] = (Exception) e;
latch.countDown();
});
thread.start();
latch.await(); // 等待异常处理完成
// 主线程处理异常
if (exceptionHolder[0] != null) {
System.out.println("主线程捕获到异常: " + exceptionHolder[0].getMessage());
}
}
}
// 输出:
// 线程 Thread-0 抛出异常
// 主线程捕获到异常: / by zero
2.2 全局默认Handler
public class GlobalExceptionHandler {
public static void main(String[] args) {
// 设置全局默认异常处理器
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("全局捕获异常: " + t.getName());
e.printStackTrace();
// 可以记录日志、发送告警等
});
new Thread(() -> {
throw new RuntimeException("子线程异常");
}).start();
new Thread(() -> {
int result = 10 / 0;
}).start();
}
}
优点:
- 无需修改业务代码,统一处理未捕获异常
- 适合全局日志记录、监控告警
缺点:
- 只能捕获未捕获的异常(如果子线程内部已经catch,则不会触发)
- 无法获取返回值
方案三:Future.get()(推荐)
原理:使用线程池的submit()方法,返回Future对象,通过get()获取异常。
public class FutureExceptionDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交Callable任务
Future<Integer> future = executor.submit(() -> {
System.out.println("子线程开始执行");
int result = 10 / 0; // 抛出异常
return result;
});
try {
// 调用get()会重新抛出异常(包装为ExecutionException)
Integer result = future.get();
} catch (ExecutionException e) {
System.out.println("捕获到子线程异常: " + e.getCause().getMessage());
e.getCause().printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
// 输出:
// 子线程开始执行
// 捕获到子线程异常: / by zero
// java.lang.ArithmeticException: / by zero
源码分析:
// FutureTask.get()源码(简化)
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L); // 等待任务完成
return report(s);
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x; // 正常返回结果
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x); // 抛出包装后的异常
}
优点:
- 标准API,使用简单
- 同时支持获取返回值和异常
- 适合需要等待任务结果的场景
缺点:
- 必须使用线程池(不适合new Thread()场景)
- get()是阻塞调用,可能影响性能
方案四:CompletableFuture(现代化方案)
原理:使用CompletableFuture的异步编程模型,支持链式异常处理。
public class CompletableFutureExceptionDemo {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("子线程执行中...");
int result = 10 / 0; // 抛出异常
return result;
});
// 方式1: exceptionally处理异常
future.exceptionally(ex -> {
System.out.println("捕获到异常: " + ex.getMessage());
return null; // 返回默认值
});
// 方式2: handle同时处理正常结果和异常
future.handle((result, ex) -> {
if (ex != null) {
System.out.println("异常处理: " + ex.getCause().getMessage());
return -1;
}
return result;
});
// 方式3: whenComplete观察结果(不改变结果)
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("任务失败: " + ex.getMessage());
} else {
System.out.println("任务成功: " + result);
}
});
Thread.sleep(1000); // 等待异步任务完成
}
}
高级用法:组合多个任务
public class CompletableFutureChainDemo {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
// 任务1
return getUserId();
}).thenApply(userId -> {
// 任务2
return queryUserInfo(userId);
}).thenAccept(userInfo -> {
// 任务3
System.out.println("用户信息: " + userInfo);
}).exceptionally(ex -> {
// 统一异常处理(任何一个任务失败都会触发)
System.err.println("任务链执行失败: " + ex.getMessage());
return null;
});
}
}
优点:
- 支持链式调用,代码简洁
- 支持异步非阻塞,性能更好
- 提供丰富的异常处理API(exceptionally、handle、whenComplete)
- 支持多任务组合(thenCompose、thenCombine等)
缺点:
- JDK8+才支持
- 学习曲线稍高
方案五:线程池afterExecute钩子
原理:重写ThreadPoolExecutor的afterExecute方法,在任务执行后检查异常。
public class ThreadPoolHookDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 2, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// execute()提交的任务,异常在t参数中
if (t != null) {
System.out.println("捕获到异常(execute): " + t.getMessage());
}
// submit()提交的任务,异常封装在FutureTask中
if (r instanceof FutureTask) {
try {
((FutureTask<?>) r).get();
} catch (ExecutionException e) {
System.out.println("捕获到异常(submit): " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
};
// 测试execute()
executor.execute(() -> {
int result = 10 / 0;
});
// 测试submit()
executor.submit(() -> {
throw new RuntimeException("submit任务异常");
});
executor.shutdown();
}
}
优点:
- 统一处理线程池中所有任务的异常
- 适合监控、日志记录场景
缺点:
- 需要自定义线程池
- 无法直接传递异常给特定的调用者
方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 共享变量 | 简单场景 | 实现简单 | 需要手动管理,易出错 |
| UncaughtExceptionHandler | 全局异常处理 | 无侵入性,适合监控 | 只能捕获未捕获异常 |
| Future.get() | 需要返回值的任务 | 标准API,简单可靠 | 阻塞调用,需要线程池 |
| CompletableFuture | 异步编程 | 功能强大,非阻塞 | 学习曲线高 |
| afterExecute | 线程池监控 | 统一处理 | 需要自定义线程池 |
实战案例:批量任务异常收集
public class BatchTaskExceptionCollector {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<String>> futures = new ArrayList<>();
// 提交10个任务
for (int i = 0; i < 10; i++) {
int taskId = i;
Future<String> future = executor.submit(() -> {
if (taskId % 3 == 0) {
throw new RuntimeException("任务" + taskId + "失败");
}
return "任务" + taskId + "成功";
});
futures.add(future);
}
// 收集结果和异常
List<String> results = new ArrayList<>();
List<Exception> exceptions = new ArrayList<>();
for (int i = 0; i < futures.size(); i++) {
try {
String result = futures.get(i).get();
results.add(result);
} catch (ExecutionException e) {
exceptions.add((Exception) e.getCause());
System.err.println("任务" + i + "异常: " + e.getCause().getMessage());
}
}
System.out.println("成功: " + results.size() + ", 失败: " + exceptions.size());
executor.shutdown();
}
}
最佳实践
| 场景 | 推荐方案 |
|---|---|
| 需要任务返回值 | Future.get() 或 CompletableFuture |
| 全局异常监控 | setDefaultUncaughtExceptionHandler |
| 线程池任务监控 | 重写afterExecute |
| 异步非阻塞 | CompletableFuture |
| 简单场景 | 子线程内部try-catch + 共享变量 |
面试答题要点
- 核心思路:异常无法跨线程传播,需通过中间机制传递(共享变量、回调、Future等)
- 推荐方案:线程池场景用Future.get()或CompletableFuture,全局监控用UncaughtExceptionHandler
- Future原理:submit()返回FutureTask,异常被捕获并存储,get()时重新抛出ExecutionException
- 注意区分:execute()异常直接打印,submit()异常封装在Future中
- 现代化方案:CompletableFuture提供更强大的异步异常处理能力,支持链式调用