对ThreadLocal的使用以及内存泄漏问题的个人见解
- 前言
- 使用场景
- ThreadLocal、Thread、ThreadLocalMap的关系
- 如何设置线程本地变量
- 内存泄漏
- Entry持有ThreadLocal的弱引用的内存泄漏问题
- 不能说Entry.referent对ThreadLocal的弱引用是造成内存泄漏的原因
- 为什么说弱引用的设计解决了key和value的内存泄漏
- 总结
前言
昨天在B站看到有个博主发了个ThreadLocal存在内存泄漏的视频,就也跟着研究下源码。因为ThreadLocal在我们的项目中基本是百分百用到的。
使用场景
比如在web项目中,用户登录完成后,在业务处理中需要用到当前登录的用户信息。所有都会封装一个工具类如下:
/**
* 获取当前登录用户信息的工具类
*/
public class LoginUserUtil {
// 保存用户信息的ThreadLocalMap的key
private final static ThreadLocal<LoginUser> userInfoThreadLocal = new ThreadLocal<LoginUser>();
// 从当前线程获取用户信息
// 在业务逻辑中获取当前登录用户可以调用该方法
public static LoginUser getUser() {
return userInfoThreadLocal.get();
}
// 设置用户信息到当前线程
// 在用户登录校验拦截器中使用setUser将从token中解析到的用户信息保存到线程中
public static void setUser(LoginUser user) {
userInfoThreadLocal.set(user);
}
}
ThreadLocal、Thread、ThreadLocalMap的关系
- ThreadLocalMap是Thread的一个属性
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap 中保存的实体对象是Entry
- Entry 持有ThreadLocal的弱引用,并封装value值,上面场景示例中的LoginUser就保存在这个value
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
}
如何设置线程本地变量
# 1.从场景案例LoginUserUtil中开始跟踪代码
// 设置用户信息到当前线程
public static void setUser(LoginUser user) {
userInfoThreadLocal.set(user);
}
# 2.ThreadLocal中的set方法
public void set(T value) {
// 2.1获取当前线程
Thread t = Thread.currentThread();
// 2.2获取当前线程的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
// 如果map存在则设置值到map
if (map != null)
// 见3
map.set(this, value);
else
createMap(t, value);
}
# ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 通过hashcode获取槽位
int i = key.threadLocalHashCode & (len-1);
// 从匹配到的槽位开始遍历处理,重复会重新赋值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果没有重复则直接将值保存到当前槽位中
// 可以看到这里保存的是Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
Entry持有ThreadLocal的弱引用的内存泄漏问题
static class Entry extends WeakReference<ThreadLocal<?>>
我们首先需要知道强软弱虚4种引用的区别
可以参考文章或自己百度
https://blog.csdn.net/panyongcsd/article/details/46605613
所以弱引用在垃圾回收的时候就会被回收掉。
下面本人写了一个示例:
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
// 调用方法往当前线程中设置值
method1() ;
// 调用gc方法提醒垃圾回收器进行垃圾回收,
System.gc();
// 线程等待几秒,因为上面代码执行后垃圾回收器不会立马进行垃圾回收
Thread.sleep(3000);
// 用来查看当前线程信息
Thread currentTread = Thread.currentThread() ;
// 上面的method1方法执行完成后testLocal的强引用就不再指向堆中的ThreadLocal对象了
// 而Entry的referent是弱引用ThreadLocal对象,所以gc执行完成后Entry.referent指向null
System.out.println("断点2.这里打断点方便查看当前线程的本地变量");
// 睡眠3秒后退出
Thread.sleep(3000);
}
static void method1(){
// 方法栈中的testLocal强引用堆中的ThreadLocal对象
ThreadLocal<String> testLocal = new ThreadLocal<>();
// 设置信息到当前线程
// 这里设置完成后当前线程的threadLocals属性也就是ThreadLocalMap的table属性的槽位中会保存一个Entry
// Entry持有ThreadLocal对象的一个弱引用,即Entry.referent弱引用ThreadLocal对象
// Entry.value强引用 常量池中的"Hello ThreadLocal"
testLocal.set("Hello ThreadLocal");
// 用来查看当前线程信息
Thread currentTread = Thread.currentThread() ;
// 此时ThreadLocal对象有testLocal的强引用和Entry.referent的弱引用
System.out.println("断点1.这里打断点方便查看当前线程的本地变量");
}
}
上面代码中的两个System.out共设置了两个断点
- 断点1的当前线程本地变量,如下截图
- 断点2如下截图
- 对比断点1和断点2的当前线程的threadLocals.table[1]可以看到,在线程生命周期内,referent已经为null了,但是value的值还在,在线程池的应用中,线程会一直复用,value会一直在内存中,这块内存无法访问了,却无法回收,造成所谓的内存泄漏
不能说Entry.referent对ThreadLocal的弱引用是造成内存泄漏的原因
我们假设如果Entry对ThreadLocal是强引用的话,那如上面的示例,最终造成的不仅仅是value的内存泄漏,连key本身的ThreadLocal也会保存在当前线程的threadLocals.table[1]中(因为强引用是不会被垃圾回收期回收的),造成key的内存泄漏,所以说弱引用的设计解决了key的内存泄漏,而且还能解决value的内存泄漏。
为什么说弱引用的设计解决了key和value的内存泄漏
public void set(T value)
public T get()
public void remove()
我们可以看到ThreadLocal提供了如上3个public方法供程序员调用,查看这3个方法的源码,会发现他们都会去检查当前线程中的threadLocals.table中的Entry,如果Entry.referent为null,就会把value给清空,解决内存泄漏问题
总结
线程独享
可以看到上面的场景案例中,ThreadLocal的变量设置为静态的,这样每个线程set的时候ThreadLocalMap的key都是同一个,但这只能说明key是所有线程共享的,但是value还是每个线程独享的,因为value被封装在独立的Entry并set到每个线程自己的ThreadLocalMap类型的属性中。
进一步解决内存泄漏问题的方式:
- 有人说将ThreadLocal设置为静态变量
这样确实可以解决内存泄漏,因为静态变量会一直持有ThreadLocal的强引用,不会被垃圾回收,可以通过threadLocal访问到value值,就不存在访问不到value的内存;但是如果真的在后续的线程生命周期内,线程不需要value了,那也就没必要一直呆在内存中占用内存,比如上面的泄漏示例中,method1执行完成后,后面的程序就不需要testLocal了,那如果把testLocal设置为静态变量,testLocal和对应的value就会一直占用内存,反而有可能造成OOM。 - 有人说使用完后,调用ThreadLocal.remove方法
这个确实可以有 - 另外ThreadLocal的get和set方法也能解决,只是不及时要等到调用时释放泄漏的内存。
总结的总结
- 弱引用的设计不是产生内存泄漏的原因,而是为了解决内存泄漏
- 进一步解决内存泄漏可以在使用完后,调用ThreadLocal.remove()是最好的方法。