什么是 ThreadLocal?
ThreadLocal 是 Java 语言中提供的一种机制,用于在多线程环境下提供线程局部变量。这意味着每个线程都拥有其自己的独立变量副本,互不干扰。它常用于存储用户会话信息、事务上下文或请求ID等需要在整个请求处理链中传递的数据。
Web 环境下的 ThreadLocal 泄漏问题
在传统的 Web 应用服务器(如 Tomcat、Jetty)或现代的 Spring Boot 应用中,通常采用线程池来处理并发请求。线程池的特点是线程会被创建一次,然后反复用于处理不同的请求(即线程复用)。
问题在于:
- 当一个请求 A 结束后,如果线程 T1 中存储的 ThreadLocal 变量没有被清理。
- 线程 T1 被放回线程池。
- 当新的请求 B 到来时,线程 T1 被分配给请求 B。
- 此时,请求 B 可能会错误地访问到请求 A 遗留的旧数据(逻辑错误),更严重的是,只要线程 T1 存活,其内部的 ThreadLocalMap 就会一直持有对旧数据的引用,导致旧数据无法被垃圾回收(内存泄漏)。
虽然 ThreadLocalMap 的键是弱引用,理论上 ThreadLocal 对象本身被回收后,内存泄漏可以被避免,但由于线程池的存在,线程 T1 长期存活,其内部的强引用值域会持续持有内存,直到下次尝试访问或线程死亡,泄漏风险极大。
解决方案:使用 remove() 方法
要彻底解决这个问题,我们必须在每次请求处理完毕后,手动调用 ThreadLocal 实例的 remove() 方法。
remove() 方法的作用是:清除当前线程中该 ThreadLocal 变量的对应值,并移除 ThreadLocalMap 中对应的 Entry,从而断开所有强引用,允许垃圾回收器回收该值占用的内存。
实践:在 finally 块中确保清理
在 Web 环境中,最可靠的清理位置是 Web 过滤器(Filter)或 Spring 拦截器(Interceptor)的请求处理结束阶段,并且必须放在 finally 块中,以确保即使业务逻辑抛出异常,清理操作也能执行。
以下是一个使用 Java Servlet Filter 进行清理的示例:
import javax.servlet.*;
import java.io.IOException;
public class ContextCleanupFilter implements Filter {
// 声明一个 ThreadLocal 变量来存储请求上下文(例如,一个请求ID)
private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
public static void setRequestId(String id) {
REQUEST_ID.set(id);
}
public static String getRequestId() {
return REQUEST_ID.get();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 在请求开始时设置值
String currentId = "REQ_" + System.currentTimeMillis();
setRequestId(currentId);
System.out.println("[" + currentId + "] 请求开始,设置 ThreadLocal");
try {
// 2. 执行正常的业务逻辑(通过 chain.doFilter 传递给下游)
chain.doFilter(request, response);
} catch (Exception e) {
// 记录异常等
System.err.println("请求处理中发生异常: " + e.getMessage());
throw new ServletException(e);
} finally {
// 3. 关键步骤:清理 ThreadLocal
// 确保无论 try 块是否成功执行或抛出异常,都执行 remove()
REQUEST_ID.remove();
System.out.println("[" + currentId + "] 请求结束,清理 ThreadLocal 完成");
}
}
// 其他方法省略...
}
总结清理原则
- 设置位置: 在请求/任务开始执行的入口处设置 ThreadLocal 值。
- 清理位置: 必须在请求/任务结束时,且位于 finally 块中。
- 方法调用: 始终调用 ThreadLocal.remove() 而不是仅设置 null。
遵循上述实践可以确保当线程被回收利用时,不会携带任何残留数据,从而有效避免 Web 环境中的内存泄漏和潜在的逻辑错误。
汤不热吧