ThreadLocal ,顾名思义就是用来提供线程(Thread)内部的局部(Local)变量的,主要应用场景为在同一个线程内方便地共享变量。
例如:一次用户请求,服务器会为其开一个线程,我们在线程中创建一个 ThreadLocal,里面存请求上下文信息,这样在之后获取这些信息时就可以很方便地拿到,而不用层层显式传参。
ThreadLocal 的实现原理并不复杂,核心就是在 Thread 类里维护了一个 Map:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
然后通过 ThreadLocal 的接口读写这个变量:
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
至于 ThreadLocalMap,是一个专门为 ThreadLocal 应用场景定制的一个 HashMap,实现细节可以先不去管,Key 为 ThreadLocal 的实例,Value 为线程局部变量。所以说,ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
准备工作做好了,接下来看下 ThreadLocal 是怎么实现线程局部变量的读写的,核心代码也都很简单。
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
当 map 为空或者没有读到值时,会触发初始化逻辑:
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
initialValue() 是一个可重写的接口,用于让开发者自定义想要的初始值。
/** * Returns the current thread's "initial value" for this * thread-local variable. This method will be invoked the first * time a thread accesses the variable with the {@link #get} * method, unless the thread previously invoked the {@link #set} * method, in which case the {@code initialValue} method will not * be invoked for the thread. Normally, this method is invoked at * most once per thread, but it may be invoked again in case of * subsequent invocations of {@link #remove} followed by {@link #get}. * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; }
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
/** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
所谓内存泄漏,就是已经无法访问的内存却没有释放,ThreadLocal 使用不当时会导致内存泄漏问题。
首先看一下 ThreadLocal 运行时的内存示意图:
ThreadLocal.ThreadLocalMap的 Key 实现是弱引用,也即图中的虚线。弱引用不会阻止 GC,因此考虑下面的情况:
ThreadLocalRef 被清除了,堆中的 ThreadLocal 实例不存在强引用了,被 GC 回收。
ThreadLocalMap 里出现了一条 Key 为 null 的 Entry,后续无法读写,也无法回收,造成内存泄漏。
如果当前线程结束后被销毁,则这一块内存可以被释放;但是如果是线程池的模式,线程迟迟不结束的话,这个问题就会一直存在。
其实,ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的 get(), set(), remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value。
但这样也无法完全避免内存泄漏,因为可能上游再也不会调用get(),set(),remove()方法了,参考文章里也给出了一个 Tomcat 内存泄漏的实例: ThreadLocal 内存泄漏的实例分析 。
正确的处理方式是: 每次使用完 ThreadLocal,都调用它的 remove() 方法清除数据 ,这样才能从根源上避免内存泄漏问题。