单例服务中不应存储线程相关状态,否则易因拦截器等中途持有局部变量导致内存泄漏、数据错乱或并发污染;应通过不可变requestcontext透传上下文,禁用threadlocal/静态缓存,拦截器须无状态,压测验证清理效果。

单例服务里不该存任何线程相关状态,局部变量传递链一旦被拦截器(或过滤器、AOP切面)中途截获并持有,就可能造成值逃逸——比如把本该随请求结束就销毁的上下文对象,意外绑定到静态引用、ThreadLocal 或单例 Bean 的字段上,引发内存泄漏、数据错乱或并发污染。
识别逃逸源头:哪些地方容易“接住”局部变量
常见破坏性持有位置包括:
- ThreadLocal 未清理:拦截器中 set 了 ThreadLocal 变量,但没在 afterCompletion 或 finally 中 remove,导致后续线程复用时读到旧值;
- 静态集合缓存了 request-scoped 对象:例如在拦截器里把 HttpServletRequest 或其包装对象存进 static Map;
- 单例 Bean 持有非 final、非线程安全的字段:比如在 Service 中定义了 private UserContext context; 并在拦截器里赋值,多个请求并发写入会覆盖;
- 异步任务中错误引用主线程局部变量:如在 preHandle 里构造了一个 DTO,又把它传给 @Async 方法,而 DTO 内部含 ServletRequest 相关引用。
重构传递链:用显式、不可变、一次性的参数替代隐式持有
核心原则是:让上下文“流经”而非“驻留”。不靠中间件保存状态,而是通过方法签名逐层透传。
- 将登录用户、租户 ID、追踪 ID 等关键上下文抽象为一个不可变容器类(如 RequestContext),所有业务方法都以它为第一个参数;
- 拦截器只负责构建 RequestContext,并通过 HandlerMethod 的参数解析机制(如自定义 HandlerMethodArgumentResolver)注入 Controller 方法,不存任何字段;
- Service 和 Repository 层方法签名同步升级,例如:
postService.createPost(requestContext, post),拒绝从 ThreadLocal 或静态工具类里取值; - 若必须跨异步边界,用 TransmittableThreadLocal 替代原生 ThreadLocal,并确保在 submit 前 copy 值,在回调后 clear。
拦截器自身要轻量且无状态
拦截器本身应是 stateless 的组件,禁止在类级别定义任何可变字段。所有临时数据都限定在 preHandle 方法栈帧内。
- 移除所有
private User currentUser;这类字段; - 避免在拦截器中 new 对象后长期持有(如缓存到 Map、塞进静态队列);
- 若需记录日志或埋点,直接使用 MDC.put(配合 logback 的 %X{})或通过 RequestContext 传递,而不是靠 ThreadLocal 存储后再读;
- 注册拦截器时明确路径范围,避免 /** 无差别拦截静态资源或健康检查端点,减少不必要执行面。
验证是否真正消除逃逸
上线前做三件事:
- 用 Arthas 的
ognl '@java.lang.Thread@currentThread().threadLocals'查看线程本地变量残留; - 在拦截器的 afterCompletion 方法里加断点,确认每次请求结束时没有未清理的 ThreadLocal Entry;
- 压测时用 jmap -histo 检查是否有大量 RequestContext、HttpServletRequest 包装类堆积,它们本不该长期存活。
不复杂但容易忽略:逃逸不是代码写错,而是设计契约被打破。只要坚持“上下文只传不存、只用不持”,单例服务就能干净地承载任意并发流量。










