Bootstrap

ThreadLocal 源码浅析(二)

替换无效 Entry

替换失效元素,用在对 Entry 进行 set 操作时,如果 set 的 key 是失效的,则需要用新的替换它。

这里不仅仅处理了当前的失效元素,还会将其他失效的元素进行清理,因为这里是当 key 为 null 时才进行的替换操作。

那什么时候 key 为 null 呢?这个除了主动的 remove 之外,就只有 ThreadLocal 的弱引用被 GC 掉了。

这里是在 set 操作时出现的,还出现了 key 为 null 的无效元素,代表已经之前发生过 GC 了,很可能 Entry 数组中还可能出现其他无效元素,所以源码中会出现向前遍历和向后遍历的情况。

向前遍历好理解,就是通过遍历找第一个失效元素的索引。向后遍历比较难理解,这里我先简单说一下 ThreadLocal 用的开放地址的方式来解决 hash 冲突的,具体原理我后面会在讲 hash 冲突时单独讲。

这种情况下,很可能当前的失效元素对应的并不是 hascode 在 staleSlot 的 Entry。因为 hash 冲突后,Entry 会后移,那么此元素的 hascode 对应的桶位很有可能往后移了,所以我们要向后找到它,并且和当前的 staleSlot 进行替换。

如果不进行此操作的话,很有可能在 set 操作时,在 ThreadLocalMap 中会出现两个桶位,都被某个 ThreadLocal 指向。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,                               int staleSlot) {    Entry[] tab = table;    int len = tab.length;    Entry e;     //记录失效元素的索引    int slotToExpunge = staleSlot;    //从失效元素位置向前遍历,直到当前 Entry为null才会停止遍历    for (int i = prevIndex(staleSlot, len);         (e = tab[i]) != null;         i = prevIndex(i, len))        if (e.get() == null)            //更新失效元素的索引,目的是找第一个失效的元素            slotToExpunge = i;     //从失效元素向后遍历    for (int i = nextIndex(staleSlot, len);         (e = tab[i]) != null;         i = nextIndex(i, len)) {        ThreadLocal<?> k = e.get();        //找到了对应key        if (k == key) {            //更新该位置的value            e.value = value;            //把失效元素换到当前位置            tab[i] = tab[staleSlot];            //把当前Entry移动到失效元素位置            tab[staleSlot] = e;                        //slotToExpunge是第一个失效元素的索引,若条件成立,向前没有失效元素            if (slotToExpunge == staleSlot)                //从当前索引开始,清理失效元素                slotToExpunge = i;                        // 清理失效元素,详情见清除无效Entry相关源码            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);            return;        }                //代表向前遍历没有找到第一个失效元素的位置        if (k == null && slotToExpunge == staleSlot)            //所以条件成立的i是向后遍历的的第一个失效元素的位置            slotToExpunge = i;    }        //没找到key,则在失效元素索引的位置,新建Entry    tab[staleSlot].value = null;    tab[staleSlot] = new Entry(key, value);        // 条件成立说明在找到了staleSlot前面找到了其他的失效元素    if (slotToExpunge != staleSlot)                // 清理失效元素,详情见清除无效Entry相关源码        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

复制代码

5.3 构造方法

还有一个基于 parentMap 的构造方法,由于目前仅在创建 InheritableThreadLocal 时调用,关于它这里不详细展开,后续会针对该类进行详解。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {    // 初始化数组    table = new Entry[INITIAL_CAPACITY];     //计算存储位置    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);     //存储元素,并将size设置为1    table[i] = new Entry(firstKey, firstValue);    size = 1;     //设置扩容阈值    setThreshold(INITIAL_CAPACITY);}

复制代码

5.4 set 方法源码

设置 key,vlaue,key 就是 ThreadLocal 对象。

private void set(ThreadLocal<?> key, Object value) {    Entry[] tab = table;    int len = tab.length;    //计算索引位置    int i = key.threadLocalHashCode & (len-1);     //从当前索引开始,直到当前Entry为null才会停止遍历    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        ThreadLocal<?> k = e.get();         //如果key存在且等于当前key,代表之前存在的,直接覆盖        if (k == key) {            e.value = value;            return;        }        //如果key不存在,说明已失效,需要替换,详情见替换无效Entry源码        if (k == null) {            replaceStaleEntry(key, value, i);            return;        }    }     //没有key则新建一个Entry即可    tab[i] = new Entry(key, value);    int sz = ++size;     //清理一些失效元素,若清理失败且达到常量中的扩容阈值,则进行rehash操作    if (!cleanSomeSlots(i, sz) && sz >= threshold)        rehash();} //删除Entry数组中所有无效的Entry并扩容private void rehash() {    //删除Entry数组中所有无效的Entry    expungeStaleEntries();    if (size >= threshold - threshold / 4)        //扩容,详情见下面的扩容机制源码        resize();}

复制代码

5.5 remove 方法源码

删除 key 对应的 entry

private void remove(ThreadLocal<?> key) {    Entry[] tab = table;    int len = tab.length;    //计算存储位置    int i = key.threadLocalHashCode & (len-1);        //从当前索引开始,直到当前Entry为null才会停止遍历    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        if (e.get() == key) {            //清除该对象的强引用,下次在通过get方法获取引用则返回null            e.clear();             //清除无效元素            expungeStaleEntry(i);            return;        }    }}

复制代码

5.6 扩容机制源码

将元素转移到新的 Entry 数组,长度是原来的两倍。

private void resize() {    //创建原数组长度两倍的新数组    Entry[] oldTab = table;    int oldLen = oldTab.length;    int newLen = oldLen * 2;    Entry[] newTab = new Entry[newLen];    int count = 0;	//计算当前元素数量    for (int j = 0; j < oldLen; ++j) {        Entry e = oldTab[j];        if (e != null) {            ThreadLocal<?> k = e.get();            if (k == null) {	//key失效则值也顺便设为null                e.value = null; 	// Help the GC            } else {                //重新计算索引位置                int h = k.threadLocalHashCode & (newLen - 1);                 //移动元素位置,若rehash后索引位置有其他元素,则继续向后移动,直至为空                while (newTab[h] != null)                    h = nextIndex(h, newLen);                newTab[h] = e;                count++;            }        }    }    setThreshold(newLen);    size = count;    table = newTab;}

复制代码

四、ThreadLocalMap 的 Hash 冲突

Java 中大部分都是使用拉链法法解决 Hash 冲突的,而 ThreadLocalMap 是通过开放地址法来解决 Hash 冲突,这两者有什么不同,下面我讲介绍一下。

1. 拉链法

拉链法也叫链地址法,经典的就是 HashMap 解决 Hash 冲突的方法,如下图。将所有的 hash 值相同的元素组成一个链表,除此外 HashMap 还进行了链表转红黑树的优化。


2. 开放地址法

原理是当发生 hash 冲突时,不引入额外的数据结构,会以当前地址为基准,通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等,ThreadLocalMap 使用的是线性探测法。

简单说,就是一旦发生了冲突,就去探测寻找下一个空的散列地址,根据上面的源码也能大致了解该处理方式。源码中的公式是key.threadLocalHashCode & (length - 1)

公式类似 HashMap 的寻址算法,详情见HashMap源码,由于数组长度是 2 的 n 次幂,所以这里的与运算就是取模,得到索引 i,这样做是为了分布更均匀,减少冲突产生。

threadLocalHashCode 源码如下:

private final int threadLocalHashCode = nextHashCode(); //初始化线程安全的Integerprivate static AtomicInteger nextHashCode =    new AtomicInteger(); //斐波那契散列乘数 --结果分布更均匀private static final int HASH_INCREMENT = 0x61c88647; //自增返回下一个hash codeprivate static int nextHashCode() {        return nextHashCode.getAndAdd(HASH_INCREMENT);}

复制代码

线性探测法的缺点:

  1. 不适用于存储大量数据,容易产生“聚集现象”;

  2. 删除元素需要清除无效元素;

五、注意事项

1. 关于内存泄漏

在了解了 ThreadLocal 的内部实现以后,我们知道了数据其实存储在 ThreadLocalMap 中。这就意味着,线程只要不退出,则引用一直存在。

当线程退出时,Thread 类会对一些资源进行清理,其中就有 threadLocals,源码如下:

private void exit() {    if (group != null) {        group.threadTerminated(this);        group = null;    }    target = null;    //加速一些资源的清理    threadLocals = null;    inheritableThreadLocals = null;    inheritedAccessControlContext = null;    blocker = null;    uncaughtExceptionHandler = null;}

复制代码

因此,当使用的线程一直没有退出(如使用线程池),这时如果将一些大对象放入 ThreadLocal 中,且没有及时清理,就可能会出现内存泄漏的风险

所以我们要养成习惯每次使用完 ThreadLocal 都要调用 remove 方法进行清理。

2. 关于数据混乱

通过对内存泄漏的解释,我们了解了当使用的线程一直没有退出,而又没有即使清理 ThreadLocal,则其中的数据会一直存在。

这除了内存泄漏还有什么问题呢?我们在开发过程中,请求一般都是通过 Tomcat 处理,而其在处理请求时采用的就是线程池。

这就意味着请求线程被 Tomcat 回收后,不一定会立即销毁,如果不在请求结束后主动 remove 线程中的 ThreadLocal 信息,可能会影响后续逻辑,拿到脏数据。

我在开发过程中就遇到了这个问题,详情见ThreadLocal中的用户信息混乱问题。所以无论如何,在每次使用完 ThreadLocal 都要调用 remove 方法进行清理。

3. 关于继承性

同一个 ThreadLocal 变量,在父线程中被设置值后,在子线程其实是获取不到的。通过源码我们也知道,我们操作的都是当前线程下的 ThreadLocalMap ,所以这其实是正常的。

测试代码如下:

public class FuXing {     /**     * 初始化ThreadLocal     */    private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();     public static void main (String[] args) {        myThreadLocal.set("father thread");        System.out.println(myThreadLocal.get()); 	//father thread         new Thread(()->{            System.out.println(myThreadLocal.get());	//null        },"thread 1").start();    }}

复制代码

那么这可能会导致什么问题呢?比如我们在本服务调用外部服务,或者本服务开启新线程去进行异步操作,其中都无法获取 ThreadLocal 中的值。

虽然都有其他解决方法,但是有没有让子线程也能直接获取到父线程的 ThreadLocal 中的值呢?这就用到了 InheritableThreadLocal。

public class FuXing {     /**     * 初始化ThreadLocal     */    private static final InheritableThreadLocal<String> myThreadLocal             = new InheritableThreadLocal<>();     public static void main (String[] args) {        myThreadLocal.set("father thread");        System.out.println(myThreadLocal.get()); 	//father thread         new Thread(()->{            System.out.println(myThreadLocal.get());	//father thread        },"thread 1").start();    }}

复制代码

InheritableThreadLocal 就是继承了 ThreadLocal,在创建和获取变量实例 inheritableThreadLocals 而不再是 threadLocals,源码如下。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {     protected T childValue(T parentValue) {        return parentValue;    }     ThreadLocalMap getMap(Thread t) {       return t.inheritableThreadLocals;    }     void createMap(Thread t, T firstValue) {        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }}

复制代码

总结

本文主要讲述了 ThreadLocal 的使用以及对其源码进行了详解,了解了 ThreadLocal 可以线程隔离的原因。通过对 ThreadLocalMap 的分析,知道了其底层数据结构和如何解决 Hash 冲突的。

最后通过对 ThreadLocal 特点的分析,了解到有哪些需要注意的点,避免以后开发过程中遇到类似问题,若发现其他问题欢迎指正交流。

;