关于threadLocal的使用中出现NullPointException的解决方案

本文深入探讨了ThreadLocal在项目日志拦截器中的应用,详细解释了如何通过ThreadLocal记录请求处理的时间消耗,并针对可能出现的NullPointerException提供了有效的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在项目日志拦截器中使用了threadlocal,在记录日志时,

private static final ThreadLocal<Long> startTimeThreadLocal =
			new NamedThreadLocal<Long>("ThreadLocal StartTime");
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
			Object handler) throws Exception {
		if (logger.isDebugEnabled()){
			long beginTime = System.currentTimeMillis();//1、开始时间  
	        startTimeThreadLocal.set(beginTime);		//线程绑定变量(该数据只有当前请求的线程可见)  
	        logger.debug("开始计时: {}  URI: {}", new SimpleDateFormat("hh:mm:ss.SSS")
	        	.format(beginTime), request.getRequestURI());
		}
		return true;
	}
@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
			Object handler, Exception ex) throws Exception {
		
		// 打印JVM信息。
		if (logger.isDebugEnabled()){
			long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间)  
			long endTime = System.currentTimeMillis(); 	//2、结束时间  
	        logger.debug("计时结束:{}  耗时:{}  URI: {}  最大内存: {}m  已分配内存: {}m  已分配内存中的剩余空间: {}m  最大可用内存: {}m",
	        		new SimpleDateFormat("hh:mm:ss.SSS").format(endTime), DateUtil.formatDateTime(endTime - beginTime),
					request.getRequestURI(), Runtime.getRuntime().maxMemory()/1024/1024, Runtime.getRuntime().totalMemory()/1024/1024, Runtime.getRuntime().freeMemory()/1024/1024, 
					(Runtime.getRuntime().maxMemory()-Runtime.getRuntime().totalMemory()+Runtime.getRuntime().freeMemory())/1024/1024); 
	        //删除线程变量中的数据,防止内存泄漏
	        startTimeThreadLocal.remove();
		}
由于过滤器filter的拦截是的没有直接访问 preHandle方法,所以 ThreadLocal没有set值,就直接访问 afterCompletion从ThreadLocal中get值,导致ThreadLocal底层使用的map为空,执行initialValue方法并抛出NullPointException,根据jdk文档推荐做法

Method Detail

  • initialValue
    protected T initialValue()
    Returns the current thread's "initial value" for this thread-local variable. This method will be invoked the first time a thread accesses the variable with the  get()  method, unless the thread previously invoked the  set(T)  method, in which case the  initialValue  method will not be invoked for the thread. Normally, this method is invoked at most once per thread, but it may be invoked again in case of subsequent invocations of  remove()  followed by  get() .

    This implementation simply returns null; if the programmer desires thread-local variables to have an initial value other than nullThreadLocal must be subclassed, and this method overridden. Typically, an anonymous inner class will be used.

    Returns:
    the initial
    value for this thread-local
    采取如下解决方案,事实证明,该initialValue确实执行,并不在抛出NullPointException,但另外在项目中就存在为什么被过滤器拦截后执行发生变化,不是先执行prehandle再执行aftercompletion,而是出现反复执行 aftercompletion的问题待解决
    private static final ThreadLocal<Long> startTimeThreadLocal =
    			new NamedThreadLocal<Long>("ThreadLocal StartTime"){
    		protected Long initialValue() {
    			Logger logger = LoggerFactory.getLogger(getClass());
    			logger.info("ThreadLocal StartTime initialValue");
    			long beginTime = System.currentTimeMillis();
    			return beginTime;
    			
    		};
    	};




<think>嗯,用户这次想深入了解Java ThreadLocal的内存泄漏问题及解决方案。从引用内容看,用户已经查阅了一些资料,对ThreadLocalMap的设计和弱引用机制有一定了解,但需要更系统的解释。 用户可能是个Java开发者,遇到了内存泄漏的实际问题,或者正在准备面试。ta特意强调“场景”和“解决方案”,说明需要实用性的指导,而不仅仅是理论。考虑到ThreadLocal在连接池、会话管理中的广泛应用,用户很可能在开发Web应用或中间件。 ThreadLocal内存泄漏的核心在于ThreadLocalMap的Entry设计:key是弱引用ThreadLocal对象,value是强引用实际存储对象。当外部强引用消失时,key被GC回收但value仍被线程持有。如果线程来自线程池(长期存活),这些僵尸Entry会累积导致内存泄漏。 解决方案要分层次说明:首先是编码规范(用后remove),其次是设计层面(用static final修饰ThreadLocal)。特别要注意用户引用的[^1][^2][^3]都提到remove的重要性,这点必须强调。另外补充一个容易被忽略的点:线程池场景下,即使remove也可能因线程复用导致数据错乱,需要在任务结束时清理。 最后抛出问题要关联实际场景,比如Spring框架的RequestContextHolder如何避免泄漏,这样能帮用户建立知识连接。技术深度上可以提到JDK开发者曾考虑过自动清理机制(如启发式扫描),但最终因性能问题放弃,所以责任在开发者。</think>### Java ThreadLocal 内存泄漏问题及解决方案 ThreadLocal 的内存泄漏问题源于其内部类 `ThreadLocalMap` 的特殊设计,主要涉及**弱引用键**与**强引用值**的冲突。以下是核心机制及解决方案: --- #### 一、内存泄漏产生原因 ```mermaid graph LR A[ThreadLocal对象] --弱引用--> B(ThreadLocalMap.Entry.key) C[用户存储的对象] --强引用--> D(ThreadLocalMap.Entry.value) E[线程Thread] --持有--> F(ThreadLocalMap) ``` 1. **键的弱引用特性** - `ThreadLocalMap` 的键(Key)是对 `ThreadLocal` 对象的**弱引用** - 当外部强引用消失时(如 `threadLocal = null`),`ThreadLocal` 对象会被 GC 回收 - 后果:Entry 中的 Key 变为 `null`,但 Value 仍存在强引用[^1][^2] 2. **值的强引用问题** - Entry 的 Value 是用户存储对象的**强引用** - 线程(尤其是线程池中的长生命周期线程)会持续持有 `ThreadLocalMap` - 后果:Key 为 `null` 的 Entry 无法被自动清理,导致 Value 对象无法回收[^3] 3. **累积性泄漏** - 线程多次使用 ThreadLocal → 产生多个无效 Entry - 例如:线程池中复用线程时,未清理的 Entry 会持续累积[^2] --- #### 二、解决方案 ##### 1. 强制清理:使用后立即调用 `remove()` ```java public void useThreadLocal() { ThreadLocal<String> threadLocal = new ThreadLocal<>(); try { threadLocal.set("data"); // 业务逻辑... } finally { threadLocal.remove(); // 关键:强制移除Entry } } ``` - **作用**:显式删除当前线程的 Entry,断开 Value 的强引用链 - **场景**:必须放在 `finally` 块中,确保异常时也能执行[^1][^3] ##### 2. 避免重复创建:声明为 `static final` ```java private static final ThreadLocal<User> userHolder = new ThreadLocal<>(); ``` - **作用**: - `static`:保证 ThreadLocal 对象始终有类加载器的强引用 - `final`:防止意外置空导致 Key 被回收 - **效果**:Key 永不回收,避免产生僵尸 Entry[^2] ##### 3. ThreadLocalMap 的自清理机制(JDK优化) - **启发式扫描**:调用 `set()`/`get()` 时触发探测性清理 - 扫描到 `key==null` 的 Entry 时,将其 Value 置空 - **扩容时全清理**:Map 扩容时遍历所有 Entry 清除无效项 - **局限**:被动清理不可靠,仍需主动 `remove()`[^1] --- #### 三、典型泄漏场景 | **场景** | **泄漏原因** | **风险等级** | |-------------------------|------------------------------------------|-------------| | 线程池任务使用 ThreadLocal | 线程复用导致 Entry 累积 | ⚠️⚡️ 高危 | | Web 应用的请求上下文 | 未清理的 Session/User 对象占用内存 | ⚠️⚡️ 高危 | | 静态工具类中的临时存储 | 未用 static final 修饰 ThreadLocal | ⚠️ 中危 | > **案例**:Tomcat 线程池处理 HTTP 请求时,若未在拦截器中调用 `remove()`,每次请求会残留用户数据,最终 OOM[^3]。 --- #### 四、最佳实践总结 1. **强规则** - 每次使用后必须调用 `threadLocal.remove()` - 将 ThreadLocal 声明为 `private static final` 2. **辅助手段** - 代码扫描工具检测未清理的 ThreadLocal - 内存监控:关注 `ThreadLocalMap` 的堆内存占用 3. **替代方案** ```java // Java 9+ 的清理增强 try (ThreadLocal.Holder<String> holder = ThreadLocal.withInitial(() -> "data")) { // 自动清理作用域 } ``` > 通过主动清理 + 强引用保持,可彻底避免内存泄漏[^1][^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值