一、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.threadLocalsThreadLocalMap getMap(Thread t) {    return t.threadLocals;}// 调用ThreadLocalMap构造方法创立ThreadLocalMapvoid createMap(Thread t, T firstValue) {    t.threadLocals = new ThreadLocalMap(this, firstValue);}// ThreadLocalMap构造方法,传入firstKey, firstValueThreadLocalMap(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在桶中的地位:71在桶中的地位:142在桶中的地位:53在桶中的地位:124在桶中的地位:35在桶中的地位:106在桶中的地位:17在桶中的地位:88在桶中的地位:159在桶中的地位:610在桶中的地位:1311在桶中的地位:412在桶中的地位:1113在桶中的地位:214在桶中的地位:915在桶中的地位: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...