ThreadLocal 类浅析(二)

阿凡达2018-06-28 09:11

ThreadLocal的角度来看,其代码并不复杂,主体代码是对ThreadLocalMap set get 操作,代码的复杂性主要封装在ThradLocalMap中。我们慢慢来看。

先看ThradLocalMapget方法

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {     int i = key.threadLocalHashCode & (table.length - 1);     Entry e = table[i];     if (e != null && e.get() == key)         return e;     else         return getEntryAfterMiss(key, i, e); }

getEntryAfterMiss函数的源 /**  * Version of getEntry method for use when key is not found in  * its direct hash slot.  *  * @param  key the thread local object  * @param  i the table index for key's hash code  * @param  e the entry at table[i]  * @return the entry associated with key, or null if no such  */ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {     Entry[] tab = table;     int len = tab.length;     while (e != null) {         ThreadLocal<?> k = e.get();         if (k == key)             return e;         if (k == null)
            // 清楚过期的Entry,同时释放该对象中value的引用。
            expungeStaleEntry(i);
        else
            // hash值有冲突,采取线性探查法,依次向后查找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

 

expungeStaleEntry函数的源

/**
 * 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
    //释放value 的引用,使其内存能被及时回收。
    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;
}

ThreadLocalMapgetEntry函数的流程:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e
  2. 如果enull或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询。
首先,对于“ThreadLocal<?> k = e.get()“ 得到的k,为何要判断 k == null 呢?因为EntryThreadLocal的引用为弱引用,其可能会被JVM回收。各类之间的引用如下图:
keyJVM回收的时候,我们就无法通过key来访问value,如果该线程长时间没有结束,value就无法释放,这时可能会出现内存泄漏。但实际上并不会出现内存泄漏。在上面的expungeStaleEntry方法中tab[staleSlot].value = null;tab[staleSlot] = null;这两行代码会在key等于null的时候将value 置为null,实现某种程度上的手工释放。
也就是说,在调用get方法时,ThreadLocalMap会对底层的数组数据进行检查,对其中无效的数据进行清理。
再来看下ThreadLocalMapset方法

private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at

            // least as common to use set() to create new entries as

            // it is to replace existing ones, in which case, a fast

            // path would fail more often than not.

            //得到应该与运算之后应该得到的下

            Entry[] tab = table;

            int len = tab.length;

            int i = key.threadLocalHashCode & (len-1);

            /*得到entry,如果enull,调用父类Referenceget方法得到 ThreadLocal对象,虽然下标相同。但是很可能不是同一个ThreadLocal对象,

如果是同一个象,k==key。就替Entry里面的value值,该下标的对象knull。就放入改位置,如果有其他的,就往下一个i+1位置上找           

            */

            for (Entry e = tab[i];

                 e != null;

                 e = tab[i = nextIndex(i, len)]) {

                ThreadLocal k = e.get();

                if (k == key) {

                    e.value = value;

                    return;

                }

                if (k == null) {

                                /*无效的值进行替在替换的过程中也会对键为nullEntry进行清理*/

                    replaceStaleEntry(key, value, i);

                    return;

                }

            }

            /*如果算后的坐标获取到的entrynull,就new一个Entry对象并保存进去,然后调用cleanSomeSlots()table进行清理,如果没有任何Entry被清理,并且表的size阈值,就会rehash()方法。

cleanSomeSlots()expungeStaleEntry清理过时Entryrehash则会调用expungeStaleEntries()方法清理所有的旧的Entry,然后在size大于阈值3/4时调用resize()方法容。代如下*/

            tab[i] = new Entry(key, value);

            int sz = ++size;

            if (!cleanSomeSlots(i, sz) && sz >= threshold)

                rehash();

        }

 

体思路大体如此,其中cleanSomeSlots log2 NEntry项进行清理,衡清理的速度和内存放的情况。rehash 函数确保了hash有足的空,不致于繁的冲突。

最后再来看下 ThreadLocal ThreadLocalMapremove方法。
/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * {@code initialValue} method in the current thread.
 *
 * @since 1.5
 */
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
/**
 * 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;
        }
    }
}
   为什么要调用remove方法呢?线程消亡后垃圾搜集器不是会自动回收垃圾的吗?因为在使用线程池的时候,线程在使用完后并不会消亡,而是会回到线程池中等待。这时手动的调用ThreadLocal的remove方法,有利于垃圾的技术回收,这是一个较好的习惯。
注意事项:
1.ThreadLocalMap 对过期数据的清除依赖于 set和 get函数,所以在不能及时调用set 或 getEntry函数的情况下需要手动的调用remove函数,手动删除不再需要的的ThreadLocal,防止内存泄漏。
2. JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
3.ThreadLocal类似于全局变量,会降低代码的可重用行,并在类之间引入隐含的耦合性。
常用的场景:
防止对可变的单实例或全局变量进行共享。
总结:
在我们调用set或者get的时候,ThreadLocal会自动的清楚key为null的值,不会造成内存泄露。而当使用线程池的时候,我们应该在改线程使用完该ThreadLocal的时候自觉地调用remove方法清空Entry,这会是一个非常好的习惯。 
被废弃了的ThreadLocal所绑定对象的引用,会在以下4情况被清理。
Thread结束时。
当Thread的ThreadLocalMap的threshold超过最大值时。rehash
向Thread的ThreadLocalMap中存放一个ThreadLocal,hash算法没有命中既有Entry,而需要新建一个Entry时。
手工通过ThreadLocal的remove()方法或set(null)。
最后放一张图(来自网络),帮助大家理解。

相关阅读:ThreadLocal 类浅析(一

本文来自网易实践者社区,经作者李文帅授权发布。