简书 涤生。 转载请注明原创出处,谢谢!
ThreadLocal大家应该不陌生,经常在一些同步优化中会使用到它。很多地方叫线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。也就是对于同一个ThreadLocal,每个线程通过get、set、remove接口操作只会影响自身线程的数据,不会干扰其他线程中的数据。
ThreadLocal是怎么实现的呢?
ThreadLocal又有哪些误区呢?
从ThreadLocal的set方法说起,set是用来设置想要在线程本地数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。
/**
* 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);
}
那ThreadLocalMap又是什么?跟线程有什么关系?可以看到ThreadLocalMap其实是线程自身的一个成员属性threadLocals的类型。也就是线程本地数据都存在这个threadLocals应用的ThreadLocalMap中。
/**
* 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;
}
我们再接着看看ThreadLocalMap,跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
我们再看看Entry的定义,Entry继承自WeakReference(这么做目的是什么,后面会讲到),构造方法有两个参数一个是threadLocal对象,一个是线程本地的变量。
/**
* 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;
}
}
看到这里大家应该就明白了,每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储以及交互访问。
上文也提到了,Entry继承自WeakReference,大家都知道WeakReference(弱引用)的特性,只要从根集出发的引用中没有有效引用指向该对象,则该对象就可以被回收,这里的有效引用并不包含WeakReference,所以弱引用不影响对象被GC。 这里被WeakReference引用的对象是哪个呢?可以看Entry的构造方法,很容易看出指的是ThreadLocal自身,也就是说ThreadLocal自身的回收不受ThreadLocalMap的这个弱引用的影响,让用户减轻GC的烦恼。
但是不用做些什么吗?这么简单? 其实不然,ThreadLocalMap还做了其他的工作,试想一下,ThreadLocal对象如果外界没有有效引用,是能够被GC,但是Entry呢?Entry也能自动被GC吗,当然不行,Entry还被ThreadLocalMap的table数组强引用着呢。 所以ThreadLocalMap该做点什么? 我看看ThreadLocalMap的expungeStaleEntry这个方法,这个方法在ThreadLocalMap get、set、remove、rehash等方法都会调用到,看下面标红的两处代码,第一处是将remove的entry赋空,第二次处是找到已经被GC的ThreadLocal,然后会清理掉table数组对entry的引用。这样entry在后续的GC中就会被回收。
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
是不是这样就万事大吉了呢,不用担心GC问题了呢? 没那么简单,还是有点坑: 这里的坑与WeakHashMap垃圾回收原理中所说的类似,如果数据初始化好之后,一直不调用get、set等方法,这样Entry就一直不能回收,导致内存泄漏。所以一旦数据不使用最好主动remove。
有些同学问,如果线程资源回收了,还会泄漏吗? 当然不会,线程回收后,存在线程相关的ThreadLocalMap也会被回收,线程相关的本地数据自然也会回收。但是不要忘记ThreadLocal的使用场景,就是用来存储线程本地变量,大部分场景中,线程都是一直存活或者长时间存活。
一般在连接池优化上会使用到ThreadLocal,在多线程获取连接池时,会有同步操作,影响性能。如果使用ThreadLocal,每个线程使用自己的独立连接,性能会有一定的提升。
希望这遍文章能够提升大家对ThreadLocal的理解,避免踩坑,使用起来也更加了然于胸。