关于java:ThreadLocal底层实现原理详解

57次阅读

共计 14786 个字符,预计需要花费 37 分钟才能阅读完成。

一、ThreadLocal 简介

ThreadLocal顾名思义能够依据字面意思了解成线程本地变量。也就是说如果定义了一个 ThreadLocal,每个线程都能够在这个ThreadLocal 中读写,这个读写是线程隔离的,线程之前不会有影响。

每个 Thread 都保护本人的一个ThreadLocalMap ,所以是线程隔离的。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

通过这个 ThreadLocalMap 实现数据的读写,既然是 Map 必定有 keyvalue,然而这个 ThreadLocalMapkey能够简略的看成是 ThreadLocal,理论是并不是ThreadLocal 的自身,而是它的一个 弱援用

二、ThreadLocal 学习纲要

学习纲要思维导图如下图:

三、ThreadLocal 办法和成员变量

API

ThreadLocalAPI 很少就蕴含了 4 个,别离是get()set()remove()withInitial(),源码如下:

public T get() {}

public void set(T value){}

public void remove(){}

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
  • get():获取以后线程对应的 ThreadLocalMap 存储的值,key为以后 TheadLocal(理论为TheadLocal 的弱援用),也就是获取以后线程本地变量的值。
  • set(T value):给以后线程对应的 ThreadLocalMap 的设置值,也就是给以后线程本地变量设置值。
  • remove():革除火线程对应的 ThreadLocalMap 存储的TheadLocal,也就是革除以后线程本地变量的值。
  • withInitial():用于创立一个线程局部变量,变量的初始化值通过调用 Supplier 的 get 办法来确定

成员变量

// 调用 nextHashCode()办法获取下一个 hashCode 值,用于计算 ThreadLocalMap.tables 数组下标
// key.threadLocalHashCode & (len - 1)
private final int threadLocalHashCode = nextHashCode();

// 原子类,用于计算 hashCode 值
private staitc AmoicInteger nextHashCode = new AmoicInteger();

// hash 增量值,斐波那契数也叫黄金分割数,能够让 hash 值散布十分平均
private static final int HASH_INCREMENT = 0x61c88647;// 获取下一个 hashCode 值办法,只用原子类操作
private static int nextHashCode () {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

四、ThreadLocalMap

ThreadLocalMapThreadLocal 类的一个动态外部类,在下面有说到每个线程都保护着一个ThreadLocalMap,这个`ThreadLocalMap 就是用来贮存数据的。

ThreadLocalMap外部保护着一个 Entry 节点,这个节点继承了 WeakReference 类,泛型为 ThreadLocal 示意是弱援用,节点外部定义了一个为 Objectvalue,这个 value 就是咱们寄存的值,Entry类的构造方法只有一个,传入 keyvalue,这个 key 就是 ThreadLocal,理论为ThreadLocal 的弱援用。

static class Entry extends WeakReference<ThreacLocal<?>> {
    Object value;
    
    Entry(ThreadLocal<?> k, Object v){super(k);
        value = v;
    }
}

Thread、ThreadLocalMap、ThreadLocal 构造关系

每个 Thread 都有一个 ThreadLocalMap 变量,ThreadLocalMap外部定义了 Entry 节点类,这个节点继承了 WeakReference 类泛型为 ThreacLocal 类,节点类的构造方法ThreadLocal<?> k, Object v,所以能够失去上面的构造关系图:

GC 之后 key 是否为 null?

思考一个问题,既然 ThreadLocalMapkey是弱援用,GC之后 key 是否为 null?在搞清楚这个问题之前,咱们须要先搞清楚Java 的四种 援用类型

  • 强援用:new进去的对象就是强援用,只有强援用存在,垃圾回收器就永远不会回收被援用的对象,哪怕内存不足的时候。
  • 软援用:应用 SoftReference 润饰的对象被称为软援用,在内存要溢出的时候软援用指向的对象会被回收。
  • 弱援用:应用 WeakReference 润饰的对象被称为弱援用,只有产生垃圾回收,被弱援用指向的对象就会被回收。
  • 虚援用:虚援用是最弱的援用,用 PhantomReference 进行定。惟一的作用就是用来队列承受对象行将死亡的告诉。

这个问题的答案是不为 null,能够看上面的图:

通过上图咱们晓得 ThreadLocal 的强援用是依然存在的,所以不会被回收,不为null

ThreadLocalMap 成员变量

// 初始化容量 必须为 2 的幂,位运算取代模运算晋升计算效率,能够试 hash 值产生碰撞的概率更小,尽可能的使
// 元素在哈希表中平均的散列
private static final int INITTAL_CAPACIRY = 16;

// Entry 表
private Entry[] table;

// Entry 表寄存的元素数量
private int size = 0;

// 扩容阙值
private int threshold;

五、ThreadLocal.set()办法源码详解

set()办法用于给本地线程变量设值,咱们先来看看 set() 办法的源码,从源码来一步一步解析实现原理,源码如下:

pubic void set(T value) {
    // 获取以后线程
    Thread t = Threac.currentThread();
    // 获取以后线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果 map 不为 null,调用 ThreadLocalMap.set()办法设置值
    if (map != null)
        map.set(this, value);
    else 
        // map 为 null,调用 createMap()办法初始化创立 map
        createMap(t, value);
}

// 返回线程的 ThreadLocalMap.threadLocals
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

// 调用 ThreadLocalMap 构造方法创立 ThreadLocalMap
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLocalMap 构造方法,传入 firstKey, firstValue
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化 Entry 表的容量 = 16
    table = new Entry[INITIAL_CAPACITY];
    // 获取 ThreadLocal 的 hashCode 值与运算失去数组下标
    int i = firsetKey.threadLocalHashCode & (INITAL_CAPACITY - 1);
    // 通过下标 Entry 表赋值
    table[i] = new Entry(firstKey, firstValue);
    // Entry 表存储元素数量初始化为 1
    size = 1;
    // 设置 Entry 表扩容阙值 默认为 len * 2 / 3
    setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {threshold = len * 2 / 3}
    

ThreadLocal.set()办法还是很简略的,外围办法在 ThreadLocalMap.set() 办法,ThreadLocal.set()办法流程如下:

  1. 获取以后线程的ThreadLocalMap map
  2. 如果 map 不为 null 则调用 map.set() 办法设置值。
  3. 如果 mapnull则调用 createMap 办法创立。
  4. createMap()办法通过 ThreadLocalMap 的构造方法创立,构造方法次要做了初始化 Entry[] table 容量 16,通过 ThreadLocalthreadLocalHashCode调用 nextHashCode() 办法获取 hashCode 值计算出下标,table数组通过下标赋值,初始化存储的元素数量,初始化数组扩容阙值。

ThreadLocalMap在构造方法里解决的时候用到了咱们学习纲要里说到的 hash 算法,源码如下:

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

private final int threadLocalHashCode = nextHashCode();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里最要害的就是 threadLocalHashCode 值的计算,ThreadLocal中有一个属性为 HASH_INCREMENT = 0x61c88647,没创立一个ThreadLocal 就会调用一次 nextHashCode() 办法,这个 HASH_INCREMENT 值就会增长 0x61c88647,这个值很非凡,是斐波那契数也叫黄金分割数,这个值能够让hash 散布十分平均。

能够下一个小 demo 测试一下:

public static void main(String[] args) {
    int hashCode = 0;
    int HASH_INCREMENT = 0x61c88647;
    for (int i = 0; i < 16; i++) {
        hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
        int bucket = hashCode & (16 - 1);
        System.out.println(i + "在桶中的地位:" + bucket);
    }
}

下面测试输入如下:能够看出数据在算列数组中散布的很平均。

 0 在桶中的地位:7
1 在桶中的地位:14
2 在桶中的地位:5
3 在桶中的地位:12
4 在桶中的地位:3
5 在桶中的地位:10
6 在桶中的地位:1
7 在桶中的地位:8
8 在桶中的地位:15
9 在桶中的地位:6
10 在桶中的地位:13
11 在桶中的地位:4
12 在桶中的地位:11
13 在桶中的地位:2
14 在桶中的地位:9
15 在桶中的地位:0

ThreadLocalMap.set()办法源码详解

ThreadLocalMap.set()办法分为好几种状况,次要有以下四种状况,针对不同的状况咱们通过画图来阐明。

阐明:上面所有图中,绿色块 Entry 代表失常数据,灰色代表 Entrykey值为 null,已被GC 回收,红色代表 Entrynull

第一种状况:通过 hash 计算失去的下标,该下标对应的 Entrynull

这种状况间接将该数据放入该槽位即可。

第二种状况:通过 hash 计算失去的下标,该下标对应的 Entry 不为 null,然而key 雷同:

这种状况间接该槽位的 value 值。

第三种状况:通过 hash 计算失去的下标,该下标对应的 Entry 不为 null,且key 不雷同,这种时候会遍历数组,线性往后查找,查找 Entrynull的槽位,且在找到 Entrynull之前没有遇到 key 过期的 Entry,就该数据放入该槽位中,如果遍历过程中,遇到了key 相等的槽位,间接更新 value 即可:

留神:每次循环查找都会判断 key 是否相等,如果相等则更新 value 间接返回。

第四种状况:基于第三种状况,如果在找到 Entrynull之前遇到了 key 过期的Entry,如下图:

如上图散列数组下标为 7 地位对应的 Entry 数据 keynull,阐明此数据 key 值曾经被垃圾回收掉了,此时会执行 replaceStaleEntry() 办法,该办法含意是 替换过期数据的逻辑 ,以index=7 为终点开始向前遍历,进行探测式数据清理工作。

初始化探测式清理过期数据扫描的开始地位:slotToExpunge = stateSlot = 7

以以后 stateSlot 开始向前迭代找到,找到其余过期的数据,而后更新过期数据起始扫描下标的 slotToExpunge ,直到找到了Entrynull的槽位则完结。

如果找到过期数据,持续向前迭代,直到遇到 Entry=null 的槽位则进行迭代,如下图所示,slotToExpunge被更新为 0:

上图以以后节点 index = 7 向前迭代,检测是否有过期的 Entry 数据,如果有则更新 slotToExpunge 的值,遇到 Entrynull则完结探测,以上图为例 slotToExpunge 被更新为 0。

下面向前迭代的操作是为了更新探测清理过期数据的起始地位 soltToExpunge 的值,这个值是用来判断以后过期槽位 staleSlot 之前是否还有过期元素。

接着开始 staleSolt 地位 index = 7 向后迭代,如果找到了相等 keyEntry的数据 则更新 value 值,如下图:

从以后节点 staleSolt 地位开始向后寻找 key 相等的 Entry 地位,如果找到了 key 相等的 Entry,则会替换staleSlot 元素的地位,且更新 value 值,而后进行过期 Entry 的清理工作,如下图:

如果没有找到 相等 keyEntry的数据,如下图:

从以后节点 staleSlot 向后查找 key 值相等的 Entry,如果没有找到,则会持续往后查找直到找到Entrynull进行,而后创立新的 Entry,替换stableSlot 的地位。

替换实现之后也是进行过期元素的清理工作,清理工作的办法次要有两个 expungeStaleEntrycleanSomeSlots,具体详情前面会讲到。

下面曾经图解了 set() 办法实现的原理,接下来咱们联合源码再来看看,源码如下:

private void set(ThreadLocal<?> key, Object value) {
    // 获取 Entry 表
    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)]){ThreadLocal<?> k = e.get();
        
        // 如果查找到 key 相等的 entry,则更新 value
        if (k == key) {
            v.value = value;
            return;
        }
        
        // 如果查找到为 key 为 null 的 entry,阐明 key 过期,被 GC 回收
        // 这个时候要初始化探测式清理的起始地位
        // 替换过期元素
        if (k == null) {replaceStateEntry(key, value, i);
            return;
        }
    }
    
    // 循环查找过程中,没有找到 key 相等的 entry,且没有 key 过期的 entry
    // 则新建一个 entry 放入 entry 表中
    table[i] = new Entry(key, value);
    
    // 寄存元素数量 +1
    int sz = ++size;
    // 调用启发式清理,且元素数量大于扩容阙值
    // 则调用 rehash 办法,该办法会进行 key 过期的 entry 清理工作,清理实现之后再判断是否须要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();}

下面代码流程次要如下:

  1. 首先获取 Entry 表,Entry表长度,通过 hashCode 计算下标,而后 for 循环 Entry 表。
  2. 如果循环查找过程中找到了 key 相等的 Entry 则更新 value 对应咱们下面说的 第二种状况
  3. 如果循环查找过程找到了 keynullEntry,阐明key 过期了,替换过期元素,须要初始化探测式清理的其实地位,调用 replaceStaleEntry() 办法,这个办法咱们上面再说,这个对应咱们下面说的 第四种状况。
  4. for循环查找结束,阐明在查找过程中该下标对应的 Entrynull,则在新建一个 Entry 放入该槽位,而后调用 启发式清理 工作。
  5. 如果 启发式清理 未清理工作数据,且 size 超过扩容阙值 (2/3),则调用rehash() 办法,该办法会先进行一次探测式清理,清理过期元素,清理结束之后如果size >= threshold - threshold / 4 ,则会进行扩容操作。

接下来看外围办法 replaceStaleEntry(),该办法在查找过程中遇到key = null 数据的时候会执行,该办法提供了替换过期数据的性能,能够对应下面说 第四种状况 来看,源码如下:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    // 获取 Entry 表
    Entry[] tab = table;
    // 获取 Entry 表长度
    int len = tab.length;
    Entry e;

    // 定义探测式清理起始地位 slotToExpunge = staleSlot
    int slotToExpunge = staleSlot;
    
    // 从 staleSlot 开始向前迭代查找是否有 key=null 的 entry
    // 如果有则更新 slotToExpunge
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // staleSlot 开始向后循环
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();

        // 如果查找到了 key 相等 entry
        // 则替换 staleSlot 和 i 的地位,且更新 value 的值
        if (k == key) {
            e.value = value;
            
               // 替换 staleSlot 和 i 的地位
            tab[i] = tab[staleSlot];
            // 更新 value 值
            tab[staleSlot] = e;
            
            // 如果 slotToExpunge == staleSlot,阐明向前循环的没有查找到 key 过期的 entry
            // 更新 slotToExpunge 值
            // 则会调用启动式过期清理,先会进行一遍过期元素探测操作
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果找到了 key 为 null 且向前循环的没有查找到 key 过期的 entry
        // 则更新 slotToExpunge
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 阐明没有找到 k == key 的数据,且碰到 Entry 为 null 的数据
    // 则将数据放入该槽位
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // slotToExpunge != staleSlot 阐明从 staleSlot 开始向前迭代查找有 key=null 的 entry
    if (slotToExpunge != staleSlot)
        // 启动式清理之前,先会进行一次过期元素探测,如果发现了有过期的数据就会先进行探测式清理
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

下面代码次要流程如下:

  1. 首先获取 Entry 表,Entry表长度,定义探测式清理起始地位 slotToExpunge = staleSlot
  2. 从 staleSlot 开始向前迭代查找是否有 key=nullentry,如果有则更新slotToExpunge
  3. staleSlot开始向后循环,如果查找到了 key 相等 entry,则替换staleSloti的地位,且更新 value 的值,而后判断 slotToExpunge == staleSlot,阐明向前循环的没有查找到key 过期的 entry,而后更新slotToExpunge 值,则会调用启动式过期清理,先会进行一遍过期元素探测操作,如果发现了有过期的数据就会先进行探测式清理。
  4. 如果找到了 keynull 且向前循环的没有查找到 key 过期的entry,则更新slotToExpunge
  5. 循环完结,办法没有退出,阐明没有找到 k == key 的数据,且碰到 Entry=null 的数据,则将数据放入该槽位。
  6. 最初判断 slotToExpunge != staleSlot,阐明从staleSlot 开始向前迭代查找有 key=nullentry,则调用启动式清理,在启动式清理之前,先会进行一次过期元素探测,如果发现了有过期的数据就会先进行探测式清理。

ThreadLocalMap.set()办法到这里曾经解析结束,咱们接下来看看 ThreadLocalMap 过期 key 的启发式清理流程。

ThreadLocalMap 过期 key 的启发式清理流程

下面咱们提到的 ThreadLocalMap 两种过期 key 数据清理形式:探测式清理 启发式清理

探测式清理

探测式清理办法 expungeStaleEntry,遍历散列数组,从开始地位向后探测清理过期数据,将过期数据的Entry 设置为 null,遍历过程如果遇到未过期的数据则会将此数据rehash 后从新在 table 数组中定位,如果定位的地位曾经有了元素,则会将未过期的数据放在最靠近此地位的 Entry = null 的桶中,使 rehash 后的 Entry 数据间隔正确的桶地位更近一点。这种优化会进步整个散列表查问性能。

如下图所示:

探测式清理迭代的过程中遇到了空的槽位,则终止探测,这样子一轮探测式清理就工作实现,咱们看看具体的源码实现,源码如下:

// staleSlot 探测式清理起始地位
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
    int len = tab.length;
    
    // 将起始地位置空
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 元素数量减 1
    size--;

    // 从新迭代散列,直到发现空槽
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();
        // 如果 key 过期,则清空元素,数量减 1
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 如果 key 没有过期,则从新计算 hash,从新获取下标
            int h = k.threadLocalHashCode & (len - 1);
            // 如果以后下标存在值,则寻找离抵触 key 所在 entry 最近的空槽
            if (h != i) {
                // i 地位槽置空
                tab[i] = null;

                // 寻找离抵触 key 所在 entry 最近的空槽,放入该槽
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
启发式清理

启发式清理被作者定义为:Heuristically scan some cells looking for stale entries

源码如下:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {i = nextIndex(i, len); // 从下一个地位开始
        Entry e = tab[i];
        // 遍历到 key==null 的 Entry
        if (e != null && e.get() == null) {
            n = len; // 重置 n
            removed = true; // 标记有清理元素
            i = expungeStaleEntry(i); // 清理
        }
    } while ((n >>>= 1) != 0); // log(n) 限度 -- 对数次
    return removed;
}

i 的下一个地位判断元素是否须要革除,如果遇到 key==null 的元素则会重置 n,须要革除且更新i 的值,判断且革除结束之后,n = n >>> 1直到 n = 0 则退出清理。

ThreadLocalMap.get()办法详解

下面曾经说完了 set() 办法的源码,接下来咱们看看 get() 办法的操作原理,次要蕴含两种状况,一种是 hash 计算出下标,该下标对应的 Entry.key 和咱们传入的 key 相等的状况,另外一种就是不相等的状况。

相等状况:相等状况解决很简略,间接返回value,如下图:

上图中比方 get(ThreadLocal1) 计算下标为 4,且 4 存在 Entry,且key 相等,则间接返回value = 11

不相等状况:不相等状况,先看图:

get(ThreadLocal2) 为例计算下标为 4,且 4 存在 Entry,但key 相等,这个时候则为往后迭代寻找 key 相等的元素,如果寻找过程中发现了有 key = null 的元素则回进行探测式清理操作。如下图:

迭代到 index=5 的数据时,此时 Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry() 办法,执行完后,index 5,8的数据都会被回收,而 index 6,7 的数据都会前移,此时持续往后迭代,到 index = 6 的时候即找到了 key 值相等的 Entry 数据,如下图:

ThreadLocalMap.get()源码如下:

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;
        }
    }
    // 未找到的话,则调用 setInitialValue()办法设置 null
    return setInitialValue();}

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // key 相等间接返回
    if (e != null && e.get() == key)
        return e;
    else
        // key 不相等调用 getEntryAfterMiss()办法
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
    int len = tab.length;
    
    // 迭代往后查找 key 相等的 entry
    while (e != null) {ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        // 遇到 key=null 的 entry,先进行探测式清理工作
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ThreadLocalMap 的扩容机制

ThreadLocalMap.set() 办法最初,如果执行完启发式清理工作之后,未清理任何数据,且以后散列数组中元素曾经超过扩容阙值 len*2/3,则执行rehash() 逻辑:

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

rehash()办法源码如下:

private void rehash() {
    // 先进行探测式清理工作
    expungeStaleEntries();

    // 探测式清理结束之后 如果 size >= threshold - threshold / 4
    // 也就是 size >= threshold * 3/4,也就是 size >= len * 1/2,则扩容
    if (size >= threshold - threshold / 4)
        resize();}

private void expungeStaleEntries() {Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

rehash()办法源码流程如下:

  1. 首先进行探测式清理工作
  2. 如果探测式清理工作结束之后,如果 size >= threshold - threshold / 4,也就是size >= threshold * 3/4,也就是 size >= len * 1/2,则调用resize() 扩容。

扩容办法 resize() 办法源码如下:

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) {e.value = null;} else {int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

扩容办法执行之后 tab 的大小为原先的两倍 oldLen * 2,而后变量老的散列表,从新计算hash 地位,而后放到新的散列表中,如果呈现 hash 抵触则往后寻找最近的 entrynull的槽位放入,扩容实现之后,从新计算扩容阙值。

六、ThreadLocal.get()办法源码详解

ThreadLcoal.get()办法源码详解曾经在 ThreadLocalMap.get() 办法源码解析中实现。

七、ThreadLocal.remove()办法源码详解

ThreadLocal.remove()办法流程比较简单,咱们联合源码来阐明,源码如下:

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    
    // 从 hash 获取的下标开始,寻找 key 相等的 entry 元素革除
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

ThreadLocal.remove()外围是调用 ThreadLocalMap.remove() 办法,流程如下:

  1. 通过 hash 计算下标。
  2. 从散列表该下标开始往后查 key 相等的元素,如果找到则做革除操作,援用置为 nullGC 的时候 key 就会置为null,而后执行探测式清理解决。

八、InheritableThreadLocal

咱们在应用 ThreadLocal 的时候,在异步场景下是无奈给子线程共享父线程中创立的线程正本数据的。

为了解决这个问题,JDK中还有一个 InheritableThreadLocal 类,咱们来看个例子:

public static void main(String[] args) {ThreadLocal<String> ThreadLocal = new ThreadLocal<>();
    ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    ThreadLocal.set("父类数据:threadLocal");
    inheritableThreadLocal.set("父类数据:inheritableThreadLocal");

    new Thread(new Runnable() {
        @Override
        public void run() {System.out.println("子线程获取父类 ThreadLocal 数据:" + ThreadLocal.get());
            System.out.println("子线程获取父类 inheritableThreadLocal 数据:" + inheritableThreadLocal.get());
        }
    }).start();}

下面代码输入后果为:

子线程获取父类 ThreadLocal 数据:null
子线程获取父类 inheritableThreadLocal 数据:父类数据:inheritableThreadLocal

实现原理是子线程通过父线程中调用 new Thread() 办法创立子线程,Thread#init办法在 Thread 的构造方法中被调用,init()办法中拷贝父线程数据源到子线程中,源码如下:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");
    }

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;
    tid = nextThreadID();}

InheritableThreadLocal 依然有缺点,个别咱们做异步化解决都是应用的线程池,而 InheritableThreadLocal 是在 new Thread 中的 init() 办法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

当然,有问题呈现就会有解决问题的计划,阿里巴巴开源了一个 TransmittableThreadLocal 组件就能够解决这个问题,这里就不再延长,感兴趣的可自行查阅材料。

参考:https://javaguide.cn/java/con…

正文完
 0