ThreadLocal内存泄漏

实线代表强引用,虚线代表弱引用
每一个Thread线程都会维护一个ThreadLocalMap它是线程独有的哦! key为使用弱引用的ThreadLocal实例,value作为线程变量的副本。
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

那么它为什么会有可能出现内存泄露的风险了,上述已经了解到了
ThreadLocalMap中的value部分是强引用,而key部分是弱引用,也就是说只要触发一次GC会回收掉这个
ThreadLocalMap中的key,由于key是指向value的,key,value键值对存储的类型必须要通过key才能找到这个value,当key被GC清除回收
问题来了
ThreadLocalMap的enrty的key为什么要设置成弱引用?强引用不行吗?
先看看这个ThreadLocalMap的Enrty到底怎么实现的
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
将Entry的Key设置成弱引用,在配合线程池使用的情况下可能会有内存泄露的风险。
之设计成弱引用的目的是:为了更好地对ThreadLocal进行回收
- 当我们在代码中将
ThreadLocal的强引用置为null后,这时候Entry中的ThreadLocal理应被回收了 - 但是如果Entry的key被设置成强引用则该
ThreadLocal就不能被回收,这就是将其设置成弱引用的目的。
那么你还能找到这个value吗?
如果有大量的这样的行为,堆内存不泄露才怪,知道了它可能会出现这样的问题
那么怎么样才能解决这个问题呢?
在Java持续完善这么多年的情况下,自然提供了相应的解决方案
解决方案
那就是在使用完本地线程,就要及时调用本身自带的UserThreadLocal对象的remove()方法,那么它为什么需要立即调用呢?我就要晚点删不可以吗?它又是怎么处理这个导致内存泄露的问题呢?
ThreadLocal.remove()
1.看看remove()方法的源码
/**
删除此线程局部变量的当前线程值。如果此线程局部变量随后被当前线程读取,则其值将通过调用其 initialValue 方法重新初始化,除非其值由当前线程在此期间设置。这可能会导致在当前线程中多次调用 initialValue 方法。
自从:1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
2.这个Thread.currentThread()又是啥?先看它的作用
/**
* Returns a reference to the currently executing thread object.
* 返回对当前正在执行的线程对象的引用。
* @return the currently executing thread.
/
//一不小心又来到native了,哈哈
public static native Thread currentThread();
本来就是key引用部分被GC回收了,再次返回对象的引用,通过getMap(传入引用参数),不就是去获取它的value吗
如果它不为null证明它有废弃的值存在,就删除它,就结了呀。
光说不练假把式,看看它的实际操作,看它到底有什么魔力,冒着可能会
OOM也要去用它。
附上简单的一个实现,这里在项目中简单用到的一个例子,用于获取用户信息方便随时可以获取它保存的用户信息且不会相互干扰(ThreadLocalMap),用于验证用户信息等相应方法处理。
import com.Hao.dao.pojo.SysUser;
/**
* @Project_Name blog
* @Author LH
* @Date 2021/9/4 21:37
* @TODO:本地线程类
* @Thinking:用于保存用户信息
*/
public class UserThreadLocal {
private UserThreadLocal(){}
//new一个本地线程对象
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
//放入用户信息
public static void put(SysUser sysUser){
LOCAL.set(sysUser);
}
//取出用户信息
public static SysUser get(){
return LOCAL.get();
}
//删除用户信息
public static void remove(){
LOCAL.remove();
}
}
2.在LoginInterceptor.class中放入用户信息
//希望在controller中直接获取用户信息,使用本地线程获取
//调用本地线程工具类中的put()方法放入用户信息
UserThreadLocal.put(sysUser);
3.用完本地线程ThreadLocal一定要删除,在LoginInterceptor.class中添加删除方法
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除ThreadLocal中用完的信息,就会有内存泄露的风险!!!
UserThreadLocal.remove();
}
用完就remove(),重要的事情说好多遍!