异常变量不可复用,尤其在并发场景下;因异常构造时固化堆栈快照,共享实例会导致日志中堆栈始终为创建位置而非真实出错点,掩盖故障根源。

异常变量不能复用,尤其在并发场景下——多个线程共用同一个异常实例,会导致堆栈信息完全失真,看似“抛出了异常”,实则记录的是该异常对象**创建时**的调用位置,而非实际出错位置。
为什么共享异常会丢失真实堆栈
Java 异常对象(如 RuntimeException 及其子类)在构造时会自动捕获当前线程的执行栈,调用 fillInStackTrace() 方法保存快照。这个过程只发生一次,且不可重置。
- 若使用 static final 声明一个异常常量(如
MyException x1 = new MyException("X1")),它的堆栈信息在类加载时就已固化 - 后续所有
throw x1操作,都只是把同一个堆栈快照反复抛出,与当前线程、当前方法无关 - 多线程环境下,不同线程在不同位置 throw 同一个异常,日志里却显示完全一致的堆栈路径,彻底掩盖真实故障点
典型错误模式识别
以下代码结构极易引发“克隆陷阱”:
- 定义全局异常常量(
public static final MyException ERR_TIMEOUT)并在各 service 层直接 throw - 在工具类中缓存异常实例用于“快速返回”,例如
if (invalid) throw CACHED_ILLEGAL_ARG; - 将异常作为方法返回值(非 throw),由调用方统一抛出——此时异常早已脱离原始上下文
正确做法:每次异常都应是新对象
确保每个异常抛出动作都对应一次独立构造:
- 用
throw new MyException("timeout"),而不是throw MyExceptionContext.TIMEOUT - 若需复用错误语义,可封装工厂方法:
MyException.timeout(String op),内部始终new - 对已有异常做增强(如添加 cause),应使用带
Throwable cause的构造函数,而非赋值修改字段 - 避免在 catch 块中仅调用
e.printStackTrace()后静默吞掉异常;若要包装再抛,必须throw new WrapperException(e)
补充:并发下还可能放大其他副作用
异常对象虽通常不可变,但若自定义异常中包含可变字段(如 id、timestamp),多个线程并发修改会导致字段值混乱,进一步干扰问题定位。
不复杂但容易忽略——只要记住:异常不是状态容器,而是故障现场的快照凭证。每一次抛出,都该是一次新鲜的“取证”。









