ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal

1 ThreadLocal

1.1 基本理解

ThreadLocalJDK底层提供的一个解决多线程并发问题的工具类,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度,从而起到线程隔离的作用,避免了线程安全问题

1.2 应用

1.2.1 使用场景

ThreadLocal并不是为了解决多线程共享变量的问题,比如商品的库存数量这种场景下是不能使用ThreadLocal 的。ThreadLocal 是多线程都需要使用一个变量,但是这个变量的值不需要各个线程间共享,每个线程都有自己的这个变量的值。
ThreadLocal 还有一种场景是 在 API 层,我们经常需要 request 这个参数,我们可能就需要在很多场景下使用这个参数,但是每个方法都把它作为参数的话会让方法的参数过多不好维护,所以我们可以把这些 request 都对应到一个线程上面,一个线程内如果想使用这个参数,直接去取就行了。
简而言之就是每个线程拥有自己的实例,然后实例需要在对应线程的使用的多个方法中共享但是不希望被多线程共享。

主要解决2类问题:

  • 并发问题:使用 ThreadLocal 代替 Synchronized 来保证线程安全,同步机制采用空间换时间 -> 仅仅先提供一份变量,各个线程轮流访问,后者每个线程都持有一份变量,访问时互不影响。
  • 数据存储问题ThreadLocal 为变量在每个线程中创建了一个副本,所以每个线程可以访问自己内部的副本变量。

1.2.2 在 Spring 中的使用->解决线程安全问题

一般情况下,只有无状态的 Bean 才会在各个实例中共享,在 Spring 中绝大多数的 Bean 都可以声明为 singleton 单例的
比如一些 request 相关的非线程安全状态采用了ThreadLocal 让它们成为线程安全的状态
一般情况下,web 应用划分成 MVC 三层,在不同的层次中编写对应的逻辑,下层通过接口向上层开放功能调用,正常情况下,从接收请求到响应都应该属于同一个线程。而 ThreadLocal 是一个很好的机制,它为每个线程提供了一个独立的变量副本解决了变量并发访问的冲突问题,比 Synchronized 要简单且方便,可以让程序具备更高的并发性

1.2.3 SimpleDateFormat线程安全问题

SimpleDateFormat线程是不安全的

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dateFormat();
                }
            });
            thread.start();
        }
    }

    /**
     * 字符串转成日期类型
     */
    public static void dateFormat() {
        try {
            simpleDateFormat.parse("2021-5-27");
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

这里我们只启动了50个线程问题就会出现,其实看巧不巧,有时候只有10个线程的情况就会出错:

Exception in thread "Thread-40" java.lang.NumberFormatException: For input string: ""
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 at java.lang.Long.parseLong(Long.java:601)
 at java.lang.Long.parseLong(Long.java:631)
 at java.text.DigitList.getLong(DigitList.java:195)
 at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
 at java.text.DateFormat.parse(DateFormat.java:364)
 at cn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
 at cn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
 at java.lang.Thread.run(Thread.java:748)

我们可以借助线程池加上ThreadLocal来解决这个问题

public class SimpleDateFormatTest {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
        @Override
       //初始化线程本地变量
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        for (int i = 0; i < 500; i++) {
            es.execute(() -> {
               //调用字符串转成日期方法
                dateFormat();
            });
        }
        es.shutdown();
    }
    /**
     * 字符串转成日期类型
     */
    public static void dateFormat() {
        try {
           //ThreadLocal中的get()方法
            local.get().parse("2021-5-27");
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

1.3 原理

1.3.1 ThreadLocal的结构

ThreadLocal 主要分为2个部分:

  • 第一部分是它的一些成员属性,这部分主要和计算哈希值相关的。
  • 另一部分是它对外提供的几个API,这些方法可以操作它自己内部非常重要的内部类 ThreadLocalMap 所以说它才是ThreadLocal 的底层实现

不同版本 ThreeadLocal原理:

  • 早期版本: ThreadLocal 是每个 ThreadLocal 类都会去创建一个 Map,然后以线程 id作为 key,要存储的局部变量作为 value,这样就可以达到线程隔离的效果。但是这样的话,这个存储数量是Thread的数量决定,当线程销毁之后还要去维护 Map中的那份k-v让它也随之销毁。
  • 后来版本:每个线程都维护一个 ThreadlocalMap 哈希表(类似HashMap),这个哈希表的 keyThreadLocal 对象本身,value 是要存储的局部副本值,这样的话存储数量是 ThreadLocal 的数量决定的。当 Thread 销毁之后,ThreadLocalMap 也会被随之销毁,减少内存占用。

ThreadLocal内存结构图
在这里插入图片描述
ThreadLocalMap原理:
ThreadLocalMap的实现原理跟 HashMap 差不多,内部有一个 Entry 数组,一个 Entry通常至少包括key,value ,特殊的是这个Entry继承了 WeakReference 也就是说它是弱引用的所以可能会有 内存泄露 的情况。这个后面再说。ThreadLocal 负责管理 ThreadLocalMap ,包括插入,删除 等等
另一方面来说 ThreadLocal 基本上就相当于门面设计模式中的一个Facade类。key就是 ThreadLocal 对象自己,同时,很重要的一点:就ThreadLocalMap 存储在当前线程对象里面

Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值。每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

对于ThreadLocal2点需要注意:

  • ThreadLocal 实例本身是不存储值,它只是提供了一个在当前线程中找到副本值的key(它自己就是ThreadLocalMapkey)
  • ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal

1.3.2 ThreadLocal的内部类ThreadLocalMap

ThreadLocalMap 的源码稍微多了点,我们就看两个最核心的方法

  • getEntry(ThreadLocal<?> key)
  • set(ThreadLocal> key, Object value)
    这个 set() 操作和我们在集合了解的put()方式有点不一样,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。
    集合Mapput()采用的是拉链法,而ThreadLocalMapset()则是采用开放定址法,开放地址法就是不会有链式的结构,如果冲突了,以当前位置为基准再找一个判断,直到找到一个空的地址。
    点击了解更多hashmap冲突的解决方法
    set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。
    get()方法有一个重要的地方当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏
1.3.2.1 ThreadLocalMap和HashMap区别

ThreadLocalMapHashMap的功能类似,但是实现上却有很大的不同:

  • HashMap的数据结构是数组+链表
  • ThreadLocalMap的数据结构仅仅是数组
  • HashMap是通过链地址法(点击了解更多hashmap冲突的解决方法)解决hash冲突的问题
  • ThreadLocalMap是通过开放地址法来解决hash 冲突的问题
  • HashMap里面的Entry内部类的引用都是强引用
  • ThreadLocalMap里面的Entry内部类中的key是弱引用,value是强引用
1.3.2.2 链地址法和开放地址法区别

jdk 中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式

  • 链地址法
    这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同一链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用12为除数,进行除留余数法:
    在这里插入图片描述
  • 开放地址法
    这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
    比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod 10。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):
    在这里插入图片描述
    计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:
    在这里插入图片描述
    链地址法和开放地址法的优缺点
  • 开放地址法:
    容易产生堆积问题,不适于大规模的数据存储。
    散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
    删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
  • 链地址法:
    处理冲突简单,且无堆积现象,平均查找长度短。
    链表中的结点是动态申请的,适合构造表不能确定长度的情况。
    删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
    指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。
1.3.2.3 ThreadLocalMap 采用开放地址法原因
  1. ThreadLocal 中有一个属性 HASH_INCREMENT = 0x61c886470x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry table,关于这个神奇的数字google 有很多解析,这里就不重复说了
  2. ThreadLocal往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

1.3.3 ThreadLocal是如何定位数据的

由于ThreadLocalMap对象底层是用Entry数组保存数据的。
那么问题来了,ThreadLocal是如何定位Entry数组数据的?
ThreadLocalget、set、remove方法中都有这样一行代码:

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

通过keyhashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
这是一种hash算法。

接下来给大家举个例子:

假设len=16,key.threadLocalHashCode=31,
于是: int i = 31 & 15 = 15
相当于:int i = 31 % 16 = 15

计算的结果是一样的,但是使用与运算效率跟高一些。

为什么与运算效率更高?

因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。

如果使用hash算法定位具体位置的话,就可能会出现hash冲突的情况,即两个不同的hashCode取模后的值相同。
ThreadLocal是如何解决hash冲突的呢?

我们看看getEntry是怎么做的:

private Entry getEntry(ThreadLocal<?> key) {
    //通过hash算法获取下标值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果下标位置上的key正好是我们所需要寻找的key
    if (e != null && e.get() == key)
        //说明找到数据了,直接返回
        return e;
    else
        //说明出现hash冲突了,继续往后找
        return getEntryAfterMiss(key, i, e);
}
再看看getEntryAfterMiss方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //判断Entry对象如果不为空,则一直循环
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果当前Entry的key正好是我们所需要寻找的key
        if (k == key)
            //说明这次真的找到数据了
            return e;
        if (k == null)
            //如果key为空,则清理脏数据
            expungeStaleEntry(i);
        else
            //如果还是没找到数据,则继续往后找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

关键看看nextIndex方法:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。

寻找的大致过程如下图所示:
在这里插入图片描述
如果找到最后一个,还是没有找到,则再从头开始找。
在这里插入图片描述

不知道你有没有发现,它构成了一个:环形。

ThreadLocal从数组中找数据的过程大致是这样的:

  • 通过keyhashCode取余计算出一个下标。
  • 通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。
  • 如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。
  • 如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。
  • 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。
  • 直到找到第一个Entry为空为止。

1.3.4 ThreadLocal是如何扩容的

从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?

set方法中会调用rehash方法:

private void set(ThreadLocal<?> key, Object value) {
    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();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。

threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:

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);
}

调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16

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

也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10
换句话说当sz大于等于10时,就可以考虑扩容了。

rehash代码如下:

private void rehash() {
    //先尝试回收一次key为null的值,腾出一些空间
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。
如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。

计算公式如下:

16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8

也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //按2倍的大小扩容
    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; // Help the GC
            } 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;
}

resize中每次都是按2倍的大小扩容。

扩容的过程如下图所示:
在这里插入图片描述

扩容的关键步骤如下:

  • 老size + 1 = 新size
  • 如果新size大于等于老size的2/3时,需要考虑扩容。
  • 扩容前先尝试回收一次key为null的值,腾出一些空间。
  • 如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
  • 每次都是按2倍的大小扩容。

1.4 各类问题总结

1.4.1 问题1:弱引用相关问题

ThreadLocalMap 中存储实体Entry使用当前 threadLocal 实例作为key,但是这个Entry继承了弱引用WeakReference,什么是弱引用?为什么要这么设计?这样会带来什么问题?

先说下什么是强引用,什么是弱引用:
只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存
点击了解更多java中四种引用(强,软,弱,虚)类型区别

回到 ThreadLocalMap 的层面来看为啥哈希表的节点要实现WeakReference弱引用。也就是ThreadLocalMap中的key使用Threadlocal实例作为弱引用。如果一个ThreadLocal没有外部引用去引用它,那么在系统GC的时候它势必要被回收的。这样一来ThreadLocalMap中就会出现keynullentry就没有办法访问这些keynullEntryvalue。如果线程一直不能结束的话,就会存在一条强引用链:ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value永远无法被回收造成内存泄露。其实在ThreadLocalMap的设计中为了防止这种情况,也有一些防护措施,比如新增移除获取的时候都会去擦除key==nullvalue

但是这些措施并不能保证一定不会内存泄露,比如:

  • 使用了static修饰的ThreadLocal,延长了ThreadLocal的生命周期,可能会导致内存泄露
  • 分配使用了ThreadLocal又不再调用get ,set ,remove方法也会导致内存泄露。

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
官方给的说法是: 为了应对非常大和长时间的用途,哈希表使用弱引用的 key。我们假设我们自己设计的时候key 使用的强引用和弱引用

  • key使用强引用:如果引用ThreadLocal的对象ThreadLocalRef被回收了,但是ThreadLocalMap还持有ThreadLocal对象的强引用,如果没有手动删除的话ThreadLocal不会被回收,这样会导致Entry内存泄露
  • key使用弱引用:引用的ThreadLocal的对象ThreadLocalRef被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。

比较上面的2种情况,我们会发现:ThreadLocalMap的生命周期和Thread一样长,如果都没有手动删除key都会导致内存泄露。但是弱引用多了一层保障,就是value在下一次ThreadLocalMap 调用 get(),set(),remove() 的时候会被清除。
因此可知,ThreadLocal发生内存泄露的根源是由于ThreadLocal的生命周期和Thread一样长,在没有手动删除对应的key的时候就会导致内存泄露,并不是因为弱引用导致的,弱引用只是优化的方式。
综上分析:为了避免内存的泄露,每次使用完ThreadLocal的时候都需要调用 remove() 方法来擦除数据。并且大规模网站一般都会使用到线程池,如果没有及时清理的话不仅是内存泄露,业务逻辑可能也会被影响。所以养成好习惯,记得擦除数据。

图示分析:
在这里插入图片描述
如图所示存在一条引用链:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value,经过上面的讲解我们知道ThreadLocal作为Key,但是被设置成了弱引用,弱引用在JVM垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次GC的时候就会变成如下
在这里插入图片描述
TreadLocalMap中出现了KeynullEntry,就没有办法访问这些key为null的Entry的value,如果线程迟迟不结束(也就是说这条引用链无意义的一直存在)就会造成value永远无法回收造成内存泄露;如果当前线程运行结束ThreadThreadLocalMapEntry之间没有了引用链,在垃圾回收的时候就会被回收;但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束;所以还是会存在内存泄露问题;解决方法也很简单,就是在使用完之后主动调用remove()方法释放掉

1.4.2 问题2:ThreadLocal和synchronized区别

说一说ThreadLocalsynchronized的区别?
ThreadLocalsynchronized都是用来处理多线程环境下并发访问变量的问题,只是二者处理的角度不同、思路不同。
ThreadLocal 是一个类,通过对当前线程中的局部变量操作来解决不同线程的变量访问的冲突问题。所以ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作锁机制的变量是多个线程共享的。
同步机制(synchronized关键字)采用了以时间换空间的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了以空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。

1.4.3 问题3:原理总结

  • 每个Thread维护着一个ThreadLocalMap的引用
  • ThreadLocalMapThreadLocal的内部类,用Entry来进行存储
  • 调用ThreadLocalset()方法时,实际上就是往ThreadLocalMap设置值,keyThreadLocal对象,值是传递进来的对象
  • 调用ThreadLocalget()方法时,实际上就是往ThreadLocalMap获取值,keyThreadLocal对象
  • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

正是因为这几点,所以能够实现数据隔离,获取当前线程的局部变量值,和其它线程无关。

1.4.4 使用ThreadLocal时对象存在哪里

java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有变量,而堆内存中的变量对所有线程可见,可以被所有线程访问

那么ThreadLocal的实例以及它的值是不是存放在栈上呢?其实不是的,因为ThreadLocal的实例实际上也是被其创建的类持有,(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见

1.5 InheritableThreadLocal

1.5.1 线程之间怎么传递ThreadLocal对象

在实际开发中,我们经常会使用 ThreadLocal 传递日志的 requestId,以此来获取整条的请求链路记录下来方便排查问题。
然而当一个线程中开启了其它的线程,此时的 Threadlocal 里面的数据就会无法获取。比如下面的代码最开始获取到的就是Null。因为不是同一个线程,所以理所当然输出的值为Null,如果要实现父子线程通信,这个问题在Threadlocal的子类 InheritableThreadLocal 已经有对应的实现了,通过这个实现,可以实现父子线程之间的数据传递,在子线程中能够使用父线程的ThreadLocal本地变量。InheritableThreadLocal 继承了ThreadLocal并且重写了三个相关的方法,具体处理大致是之前的ThreadLocal获取 ThreadlocalMap 的时候一般都是用 this ,在这里都是Thread先获取父线程,然后将父线程的 ThreadLocalMap 传递给子线程

但是阿里巴巴编码规范插件说了啥?不要显示创建线程,请使用线程池!所以下面我们用线程池来试试!
如下代码所示,当线程池的核心线程数设置为1的时候,2次输出的结果都是 我是主线程1ThreadPoolManage 是本地写的一个线程池实现,github上有源码。原因相信都能踩到了,线程池会缓存使用过的线程,第一个任务来的时候创建一个线程,此时线程空闲了,第二次来任务还是会使用这个线程,所以就会出现下面的问题了。如何解决?阿里的transmittable-thread-local 提供了解决方案,思路是,InheritableThreadLocal虽然可以完成父子线程的传递,但是对于使用了线程池的情况线程是让线程池去创建好的,然后拿来复用的,这个时候父子线程传递 ThreadLocalMap 的引用没有意义了,应用需要的是把任务提交给线程池时候把 ThreadLocalMap 传递到任务去执行。感兴趣在阿里的github上有,已经开源的。

/**
 * ThreadLocalTestExecutor
 * @since 2019/1/23 下午11:00
 */
public class ThreadLocalExecutorTest {
    private static ThreadPoolManager threadPoolManager = ThreadPoolManager.INSTANCE;
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set("我是主线程1");
        threadPoolManager.addExecuteTask(()->{
            System.out.println(threadLocal.get());
            return null;
        });
        threadLocal.set("我是主线程2");
        threadPoolManager.addExecuteTask(()->{
            System.out.println(threadLocal.get());
            return null;
        });
        //当线程池核心线程数为1的时候2次输出都是 我是主线程1
    }
}

1.5.2 InheritableThreadLocal是如何弥补ThreadLocal不支持继承的特性,它的实现原理是啥

ThreadLocalInheritableThreadLocal本质上只是为了方便编码给的工具类,具体存数据是ThreadLocalMap 对象。
ThreadLocalMap 存的key对象是ThreadLocalvalue就是真正需要存的业务对象。
Thread里通过两个变量持用ThreadLocalMap 对象,分别为:threadLocalsinheritableThreadLocals
InheritableThreadLocal之所以能够完成线程间变量的传递,是在new Thread()的时候对inheritableThreadLocals对像里的值进行了复制。
子线程通过继承得到的InheritableThreadLocal里的值与父线程里的InheritableThreadLocal的值具有相同的引用,如果父子线程想实现不影响各自的对象,可以重写InheritableThreadLocalchildValue方法

InheritableThreadLocal 在子线程创建的时候把父线程的 ThreadLocalMap 传递给它,它继承 ThreadLocal 并重写了3个方法,并使用 Thread.inheritableThreadLocals 代替了 Thread.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);
    }
}

1.6 TransmittableThreadLocal

1.6.1 定义

TransmittableThreadLocal是阿里开源的一个类,主要目的是处理父子线程变量不能共用的情况。ThreadLocal是跟当前线程挂钩的,所以脱离当前线程它就起不了作用。

  • ThreadLocal
    目的就是为了保证当每个线程都能有一个独有的变量
  • InheritableThreadLocal
    InheritableThreadLocalThreadLocal的区别是,InheritableThreadLocal定义的变量是父->子线程可传递的。其实就是在new Thread()的时候,判断下父线程是否定义了InheritableThreadLocal,如果有,就拷贝一份ThreadLocalMap
  • TransmittableThreadLocal
    这个就是就是实现线程池中创建好的线程可以进行值传递,无法传递值的问题。
    由于线程池的中的线程只能创建一次,回到InheritableThreadLocal上面说的步骤,传递的时机只有new Thread()才会出现。队列未满,线程池到了最大核心线程数就会停止创建,在这些线程未销毁前,父线程更新InheritableThreadLocal定义的变量,线程池中的线程拿的还是之前的InheritableThreadLocal变量的值。
    TransmittableThreadLocal怎么实现线程池中线程能进行值传递呢?
    我们可以想到线程池执行原理,或者说线程执行的方法本质就是run()方法。对Runnable做了个增强(TtlRunnable)。在真正的run方法执行前,会把父线程的本地变量replay给子线程。run方法结束后,就恢复为原来状态。

InheritableThreadLocal解决父子线程的问题,它是在线程创建的时候进行复制上下文的。那么对于线程池的已经创建完了就无从下手了,所以在线程提交的时候要进行上下文的复制。这就是TransmittableThreadLocal想要解决的问题

1.6.2 pom.xml

TransmittableThreadLocal是阿里开源的一个类,因此需要阿里的pom.xml坐标

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.2.0</version>
</dependency>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值