不可变自定义运行时异常不参与主动投递,其核心价值在于以线程安全、无副作用方式封装错误上下文,供上层可靠捕获与诊断;它通过final类、final字段、不可变类型(如ImmutableList、Instant)及完整异常链,确保多线程并发读取时状态一致、诊断信息可信。
不可变自定义运行时异常本身不参与“投递”,它不被线程池或流处理机制主动传递或转发;它的作用是在异常发生时,以线程安全、无副作用的方式封装错误上下文,供上层捕获、记录或响应。在大规模并发流(如 parallelstream()、completablefuture 链、reactor 或 rxjava 流)中,真正需要安全“投递”的是异常信号的语义和诊断信息——而这正依赖异常对象本身的不可变性与设计合理性。
为什么不可变性对并发流中的异常关键
在多线程环境下,若异常对象可被修改(例如字段非 final、内部状态可变),当多个线程同时访问该异常实例(如日志打印、全局异常处理器读取字段),可能看到不一致的状态,甚至引发 ConcurrentModificationException 或掩盖真实错误值。
不可变异常确保:
- 所有字段声明为
final,构造即完成,生命周期内状态恒定 - 不持有可变对象引用(如
ArrayList、HashMap),或确保其内部也不可变(用ImmutableList等) - 即使被多个线程同时读取(如不同 worker 线程触发
catch后调用getErrorCode()),结果始终可靠
如何定义一个真正不可变的自定义 RuntimeException
以下是一个生产级示例,兼顾语义清晰、线程安全与诊断能力:
注意:不要仅靠 final 修饰字段就认为不可变——必须保证字段类型本身不可变。
public final class InventoryValidationException extends RuntimeException {
private final String code; // final String → 安全
private final long orderId; // primitive → 安全
private final ImmutableList<String> skuList; // Guava ImmutableList → 安全
private final Instant timestamp; // java.time.Instant 是不可变的
public InventoryValidationException(
String code,
String message,
long orderId,
List<String> skuList,
Throwable cause) {
super(message, cause); // 保留异常链,关键!
this.code = Objects.requireNonNull(code);
this.orderId = orderId;
this.skuList = ImmutableList.copyOf(Objects.requireNonNull(skuList));
this.timestamp = Instant.now();
}
// 只提供 getter,无 setter
public String getCode() { return code; }
public long getOrderId() { return orderId; }
public ImmutableList<String> getSkuList() { return skuList; }
public Instant getTimestamp() { return timestamp; }
}
这个类满足:
- 类声明为
final(防止子类破坏不可变性) - 所有字段
final且初始化完成 - 集合参数经
ImmutableList.copyOf()转换,杜绝外部修改影响 - 显式传入
cause,保留原始异常链,便于根因追溯
在并发流中安全“投递”异常语义的实践方式
所谓“投递”,实际是指让异常携带的信息能被正确捕获、传播、记录或反馈。在不同并发模型中策略略有差异:
-
Java Stream.parallelStream():异常会中断流,由终端操作(如
collect())所在线程捕获。此时只需确保抛出的是不可变异常,无需额外同步 -
CompletableFuture 链:使用
exceptionally()或handle()捕获;不可变异常可安全跨线程传递,避免在回调中修改其状态 -
Reactor (Mono/Flux):通过
onErrorResume()、doOnError()处理;异常对象在事件循环线程间流转,不可变性保障诊断字段始终可信 -
自定义线程池任务(Runnable/Callable):若用
execute(),异常会丢失,必须在任务内try-catch并记录不可变异常;若用submit(),则Future.get()抛出的ExecutionException包裹原异常,仍可安全提取不可变字段
配套建议:统一异常处理与日志输出
不可变异常的价值需配合合理使用才能释放:
- 在 Spring 环境中,用
@ControllerAdvice+@ExceptionHandler统一拦截,直接序列化code、orderId等字段返回前端,无需担心并发读取冲突 - 日志框架(如 Logback)记录时,避免在
toString()中做耗时或可变操作;推荐重写toString()仅拼接final字段 - 禁止在异常中缓存
ThreadLocal数据或持有Connection等资源引用——这违背异常职责,也破坏不可变前提

















