ThreadLocal概念
ThreadLocal,即线程变量,是以一个ThreadLocal对象为key、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
ThreadLocal使用场景
场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全
。
ThreadLocal解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在initialValue中new出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。
场景2,ThreadLocal用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal直接获取到,避免了传参,类似于全局变量的概念。
源码
先看下,Thread、ThreadLocal及ThreadLocalMap三者之间的关系:
我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了 ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。
而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。
而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。
这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。
get方法
public T get() {
// 获取到当前线程
Thread t = Thread.currentThread();
// 获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果线程内之前没创建过 ThreadLocalMap,就创建
return setInitialValue();
}
它利用了Thread.currentThread来获取当前线程的引用,并且把这个引用传入到了getMap
方法里面,来拿到当前线程的ThreadLocalMap。
值得注意的是,这里的ThreadLocalMap是保存在线程Thread类中的,而不是保存在ThreadLocal中的。
getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,这个方法很清楚地表明了Thread和ThreadLocalMap的关系,可以看出ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的ThreadLocalMap对象,每个线程都有ThreadLocalMap对象,而这个对象的名字就叫作 threadLocals
,初始值为null,代码如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是this,就是当前ThreadLocal的引用,这也再次体现了,在ThreadLocalMap 中,它的key的类型是ThreadLocal;而第二个参数就是我们所传入的value,这样一来就可以把这个键值对保存到ThreadLocalMap中去了。
ThreadLocalMap类,也就是Thread.threadLocals
下面我们来看一下ThreadLocalMap这个类,下面这段代码截取自定义在ThreadLocal类中的ThreadLocalMap类:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
//...
}
ThreadLocalMap类是每个线程Thread类里面的一个成员变量,其中最重要的就是截取出的这段代码中的Entry内部类。在ThreadLocalMap中会有一个 Entry类型的数组,名字叫table。可以把Entry理解为一个map,键即当前的ThreadLocal,值则为实际需要存储的变量。
ThreadLocalMap既然类似于Map,所以就和HashMap一样,也会有包括set、get、rehash、resize
等一系列标准操作。但是,虽然思路和HashMap 是类似的,但是具体实现会有一些不同。
比如其中一个不同点就是,我们知道HashMap在面对hash冲突
的时候,采用的是拉链法
。它会先把对象hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:
它采用的是线性探测法
。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是ThreadLocalMap和HashMap 在处理冲突时不一样的点。
内存泄露问题
Key的泄漏
上面分析了ThreadLocal的内部结构,知道了每一个Thread都有一个ThreadLocal.ThreadLocalMap这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了ThreadLocal之后,都会在它的ThreadLocalMap里面的Entry中去维护该ThreadLocal变量与具体实例的映射。
我们可能会在业务代码中执行了ThreadLocal instance = null
操作,想清理掉这个ThreadLocal实例,但是假设我们在ThreadLocalMap的Entry 中强引用了ThreadLocal实例,那么,虽然在业务代码中把ThreadLocal实例置为了null,但是在Thread类中依然有这个引用链的存在。
GC的时候会进行可达性分析
,它会发现这个ThreadLocal对象依然是可达的,所以对于这个ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
JDK开发者考虑到了这一点,所以ThreadLocalMap中的Entry继承了WeakReference
弱引用,代码如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了ThreadLocal的内存泄露问题。
这就是为什么Entry的key要使用弱引用的原因。
Value的泄漏
可是,如果我们继续研究的话会发现,虽然ThreadLocalMap的每个Entry都是一个对key的弱引用,但是这个Entry包含了一个对value 的强引用,还是刚才那段代码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到,value = v
这行代码就代表了强引用的发生。
正常情况下,当线程终止,key所对应的value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能ThreadLocal 以及它所对应的value早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。
为了更好地分析这个问题,我们用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用):
可以看到,左侧是引用栈,栈里面有一个ThreadLocal的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。
我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例
。
这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个Value了,此时也就发生了内存泄漏问题。
JDK同样也考虑到了这个问题,在执行ThreadLocal的set、remove、rehash
等方法时,它都会扫描key为null的Entry,如果发现某个Entry的key为 null,则代表它所对应的value也没有作用了,所以它就会把对应的value置为null,这样value对象就可以被正常回收了。
但是假设 ThreadLocal 已经不被使用了,那么实际上set、remove、rehash
方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。
如何避免内存泄露
调用ThreadLocal的remove方法。调用这个方法就可以删除对应的value对象,可以避免内存泄漏。
我们来看一下remove方法的源码:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
这样value就可以被GC回收了。