目录
- 一、ThreadLocal 有什么用
- 二、ThreadLocal 使用示例
- 三、ThreadLocal 实现原理
- 四、ThreadLocal 如何是使用弱引用解决内存泄漏问题
- 4.1、强引用内存泄漏分析
- 4.1、弱引用解决内存泄漏问题
一、ThreadLocal 有什么用
ThreadLocal 诞生于 JDK 1.2,用于解决多线程间的数据隔离问题。也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本,在 Servlet 中就会将Request 和 Response对象存入ThreadLocal,我们在当前线程任意方法中都能通过RequestContextHolder
获取到当前请求的Request 和 Response对象 。
HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
二、ThreadLocal 使用示例
public static ThreadLocal<String> tlName = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
tlName.set("张三");
new Thread(()->{
tlName.set("李四");
System.out.println(Thread.currentThread().getName()+"-" + tlName.get()); // Thread-0-李四
}).start();
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"-"+tlName.get()); // main-张三
}
这里可以看到同一个ThreadLocal
对象tlName
在不同线程中可以独立设置自己的值,线程之间不会互相影响,下面会分析实现原理。
三、ThreadLocal 实现原理
分析ThreadLocal
实现原理可以从它的set
方法入手,set
方法的源码如下:
// ThreadLocal 的set方法传入一个value
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程对象
// 调用ThreadLocal的getMap方法传入当前线程对象
// 通过当前线程对象获取到当前线程的ThreadLocalMap对象,每个线程对象中都存储了自己的ThreadLocalMap对象
ThreadLocal.ThreadLocalMap map = getMap(t);
// 这里会判断当前线程对象是否已经创建ThreadLocalMap,如果没有创建会先执行创建并且set
// 如果已经创建则将当前的ThreadLocal对象为key,传入的value为值存储到当前线程的ThreadLocalMap中。
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 这个方法就是通过线程对象点属性获取ThreadLocalMap
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,ThreadLocal为每个线程创建了一个ThreadLocalMap,在set的时候key就是当前的ThreadLocal对象,value就是set里的值,以上测试代码执行时逻辑内存空间如下图:
四、ThreadLocal 如何是使用弱引用解决内存泄漏问题
4.1、强引用内存泄漏分析
假设ThreadLocal
没有使用弱引用而是使用的强引用,则会出现内存泄漏问题,这里对原理进行分析:
现在我们只考虑tlName
这个对象,它通过new ThreadLocal<>()
开辟了一个内存空间,当某线程进行set
时,又在内存中开辟了一个空间存放ThreadLocalMap
,线程对象的threadLocals
对象指向这个ThreadLocalMap
,ThreadLocalMap
的key
是tlName
这个对象,value
是set
的值,如下:
现在如果我们在线程中执行tlName = null
,从逻辑上讲这个强引用就断开了,通过new ThreadLocal<>()
开辟的内存空间就没用了,应该属于垃圾被GC回收,但问题是线程对象并没释放,其属性threadLocals
还指向该内存空间,根据垃圾回收可达性算法,这两部分内存空间是不能被清除掉的,在使用线程池的业务中很容易出现这种问题,因为线程对象会出现复用的情况。
当然我们也可以手动将ThreadLocalMap
中对tlName
的引用删除,手动调用remove
清除即可,但是在实际工作做并不一定记得手动清除,或者因为一些异常导致没有清除,这个时候还是会存在内存泄漏,ThreadLocal
会使用弱引用来解决这个问题。
4.1、弱引用解决内存泄漏问题
要想探究这个问题要先分析一下每个线程对象的threadLocals
属性,这个属性是ThreadLocal.ThreadLocalMap
类型的,在ThreadLocal
第一次调用set
方法时创建,通过上述的ThreadLocal
实现原理可以知道,ThreadLocalMap
就是每个线程真实用来存储ThreadLocal
和 value
的,其中key
就是ThreadLocal
对象,下面来分析一下ThreadLocalMap
结构。
public class ThreadLocal<T> {
// ... ...
static class ThreadLocalMap {
// ThreadLocalMap 的Entry 对象实现了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// Entry 的构造方法 传入ThreadLocal 和value
Entry(ThreadLocal<?> k, Object v) {
// 调用父类弱引用WeakReference的构造方法将ThreadLocal传入托管给弱引用管理
super(k);
value = v;
}
}
// 多个ThreadLocal与value会存储在这个Entry数组中
private ThreadLocal.ThreadLocalMap.Entry[] table;
// 将ThreadLocal与value存储到Entry数组table中,并且会进行清理槽位判断
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (ThreadLocalMap.Entry e = tab[i];
// ... ...
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 当table中有Entry对象中的key为null时需要进行清理
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 清理槽位
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
// ... ...
}
}
通过上述代码可以看到其中Entry
就是一个弱引用,会通过弱引用WeakReference
的构造方法将ThreadLocal
传入托管给弱引用管理,也就谁说ThreadLocal
中的key
是通过弱引用指向new ThreadLocal<>()
开辟的内存空间,所以当tlName = null
时,这段内存空间由于只有弱引用指向它,经过一次GC直接就被清除了,key
自动变为null
,达到了预期的效果。
通过上图可以看到new ThreadLocal<>()
开辟的内存空间被回收了,ThreadLocalMap
中key
也变为null
,但这个Entry
对象还在table
数组中,value
张三也没有被回收,如果张三是个大对象,没用了又占据着内存空间,那么ThreadLocal
还是存在内存泄漏问题,不过上述代码中有一个调用rehash
清理槽位的逻辑,在每次set
时都会判断是否有key
为null
,如果有的话会将value
置为null
。
要想彻底解决ThreadLocal
内存泄漏问题需要手动调用ThreadLocal
提供remove
方法,或者set(null)
也行,其实我们平时写代码感觉很少主动去写tlName = null
这样的操作,但是如果tlName
声明周期只在某个方法里,方法出栈,线程还在的情况下,tlName
就不再属于GC Roots
引用了,和tlName = null
效果是一样的,可能不经意就造成内存泄漏。