ThreadLocal源码分析
ThreadLocal的创建
从ThreadLocal
的构造函数来看,ThreadLocal
实例的构造并不会做任何操作,只是为了得到一个ThreadLocal
的泛型实例,后续可以把它作为ThreadLocalMap$Entry
的键:
// 注意threadLocalHashCode在每个新`ThreadLocal`实例的构造同时已经确定了 private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // 通过Supplier去覆盖initialValue方法 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } // 默认公有构造函数 public ThreadLocal() { } 复制代码
注意threadLocalHashCode在每个新ThreadLocal
实例的构造同时已经确定了,这个值也是Entry哈希表的哈希槽绑定的哈希值。
TreadLocal的set方法
ThreadLocal
中set()
方法的源码如下:
public void set(T value) { //设置值前总是获取当前线程实例 Thread t = Thread.currentThread(); //从当前线程实例中获取threadLocals属性 ThreadLocalMap map = getMap(t); if (map != null) //threadLocals属性不为null则覆盖key为当前的ThreadLocal实例,值为value map.set(this, value); else //threadLocals属性为null,则创建ThreadLocalMap,第一个项的Key为当前的ThreadLocal实例,值为value createMap(t, value); } // 这里看到获取ThreadLocalMap实例时候总是从线程实例的成员变量获取 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 创建ThreadLocalMap实例的时候,会把新实例赋值到线程实例的threadLocals成员 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } 复制代码
上面的过程源码很简单,设置值的时候总是先获取当前线程实例并且操作它的变量threadLocals。步骤是:
- 获取当前运行线程的实例。
- 通过线程实例获取线程实例成员threadLocals(
ThreadLocalMap
),如果为null,则创建一个新的ThreadLocalMap
实例赋值到threadLocals。 - 通过threadLocals设置值value,如果原来的哈希槽已经存在值,则进行覆盖。
TreadLocal的get方法
ThreadLocal
中get()
方法的源码如下:
public T get() { //获取当前线程的实例 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } //线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals return setInitialValue(); } private T setInitialValue() { // 调用initialValue方法获取值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值 if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; } 复制代码
initialValue()
方法默认返回null,如果ThreadLocal
实例没有使用过set()
方法直接使用get()
方法,那么ThreadLocalMap
中的此ThreadLocal
为Key的项会把值设置为initialValue()
方法的返回值。如果想改变这个逻辑可以对initialValue()
方法进行覆盖。
TreadLocal的remove方法
ThreadLocal
中remove()
方法的源码如下:
public void remove() { //获取Thread实例中的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除 m.remove(this); } 复制代码
ThreadLocal.ThreadLocalMap的初始化
我们可以关注一下java.lang.Thread
类里面的变量:
public class Thread implements Runnable { //传递ThreadLocal中的ThreadLocalMap变量 ThreadLocal.ThreadLocalMap threadLocals = null; //传递InheritableThreadLocal中的ThreadLocalMap变量 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; } 复制代码
也就是,ThreadLocal
需要存放和获取的数据实际上绑定在Thread
实例的成员变量threadLocals中,并且是ThreadLocal#set()
方法调用的时候才进行懒加载的,可以结合上一节的内容理解一下,这里不展开。
什么情况下ThreadLocal的使用会导致内存泄漏
其实ThreadLocal
本身不存放任何的数据,而ThreadLocal
中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread
对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal
实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal
,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal
实例的时候,JVM的GC会回收ThreadLocalMap
中的这些Key,此时,ThreadLocalMap
中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal
中get()
、set()
、remove()
这些方法中都存在清理ThreadLocalMap
实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:
- 1、大量地(静态)初始化
ThreadLocal
实例,初始化之后不再调用get()
、set()
、remove()
方法。 - 2、初始化了大量的
ThreadLocal
,这些ThreadLocal
中存放了容量大的Value,并且使用了这些ThreadLocal
实例的线程一直处于活跃的状态。
ThreadLocal
中一个设计亮点是ThreadLocalMap
中的Entry
结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap
中的所有数据都是与Thread
的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal
在下一次调用get()
、set()
、remove()
方法就可以删除那些ThreadLocalMap
中Key为null的值,起到了惰性删除释放内存的作用。
其实ThreadLocal
在设置内部类ThreadLocal.ThreadLocalMap
中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry
类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>
。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:
public class ThreadLocalMain { private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>(); public static void main(String[] args) throws Exception { TL_1.set(1); TL_1 = null; System.gc(); Thread.sleep(300); } } 复制代码
这种情况下,TL_1这个ThreadLocal
在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap
实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference
)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal
未回收之前的值。这些被"孤立"的哈希槽Entry就是前面说到的要惰性删除的哈希槽。
ThreadLocal的最佳实践
其实ThreadLocal
的最佳实践很简单:
- 每次使用完
ThreadLocal
实例,都调用它的remove()
方法,清除Entry
中的数据。
调用remove()
方法最佳时机是线程运行结束之前的finally
代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。
父子线程数据传递InheritableThreadLocal
留待下一篇文章编写,因为InheritableThreadLocal
只能通过父子线(1->1)程传递变量,线程池里面的线程有可能是多个父线程共享的(也就是1个父线程提交的任务有可能由线程池中的多个子线程执行),因此有可能出现问题。阿里为了解决这个问题编写过一个框架-transmittable-thread-local,解决了父线程和线程池中线程的变量传递问题。
小结
ThreadLocal
线程本地变量是线程实例传递和存储共享变量的桥梁,真正的共享变量还是存放在线程实例本身的属性中。ThreadLocal
里面的基本逻辑并不复杂,但是一旦涉及到性能影响、内存回收(弱引用)和惰性删除等环节,其实它考虑到的东西还是相对全面而且有效的。