ToB企服应用市场:ToB评测及商务社交产业平台

标题: ThreadLocal 源码浅析 [打印本页]

作者: 刘俊凯    时间: 2024-6-24 11:30
标题: ThreadLocal 源码浅析
前言

多线程在访问同一个共享变量时很可能会出现并发题目,特殊是在多线程对共享变量进行写入时,那么除了加锁另有其他方法制止并发题目吗?本文将具体讲解 ThreadLocal 的使用及其源码。
一、什么是 ThreadLocal?

ThreadLocal 是 JDK 包提供的,它提供了线程当地变量,也就是说,如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每一个线程,都创建这个变量的一个当地副本。
这样可以解决什么题目呢?当多个线程操作这个变量时,实际操作的是自己线程当地内存里的数据,从而制止线程安全题目
如下图,线程表中的每个线程,都有自己 ThreadLocal 变量,线程操作这个变量只是在自己的当地内存在,跟其他线程是隔离的。

二、如何使用 ThreadLocal

ThreadLocal 就是一个简单的容器,使用起来也没有难度,初始化后仅需通过 get/set 方法进行操作即可。
如下代码,开发两个线程对 ThreadLocal 变量进行操作,获取的值是不同的。
  1. public class FuXing {
  2.     /**
  3.      * 初始化ThreadLocal
  4.      */
  5.     private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
  6.     public static void main (String[] args) {
  7.         // 线程1中操作 myThreadLocal
  8.         new Thread(()->{
  9.             myThreadLocal.set("thread 1");                //set方法设置值
  10.             System.out.println(myThreadLocal.get());        //get方法获取值"thread 1"
  11.         },"thread 1").start();
  12.         // 线程2中操作 myThreadLocal
  13.         new Thread(()->{
  14.             myThreadLocal.set("thread 2");                //set方法设置值
  15.             System.out.println(myThreadLocal.get());        //get方法获取值"thread 2"
  16.         },"thread 2").start();
  17.     }
  18. }
复制代码
三、ThreadLocal 实现原理

ThreadLocal 是如何保证操作的对象只被当前线程进行访问呢,我们通过源码一起进行分析学习。
一样平常分析源码我们都先看它的构造方法是如何初始化的,接着通过对 ThreadLocal 的简单使用,我们知道了关键的两个方法 set/get,所以源码分析也按照这个次序。
1. 构造方法

泛型类的空参构造,没有什么特殊的
2. set 方法源码

源码如下,ThreadLocalMap 是什么呢?由于比力复杂,这里先不做解释,你暂时可以理解为是一个 HashMap,此中 key 为 ThreadLocal 当前对象,value 就是我们设置的值,后面会单独解释源码。
  1. public void set(T value) {
  2.     //获取本地线程
  3.     Thread t = Thread.currentThread();
  4.     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
  5.     ThreadLocalMap map = getMap(t);
  6.     if (map != null)
  7.         //获取到则添加值
  8.         map.set(this, value);
  9.     else
  10.         //否则初始化ThreadLocalMap --第一次设置值
  11.         createMap(t, value);
  12. }
复制代码
  1. void createMap(Thread t, T firstValue) {
  2.     t.threadLocals = new ThreadLocalMap(this, firstValue);
  3. }
复制代码
3. get 方法源码
  1. public T get() {
  2.     //获取本地线程
  3.     Thread t = Thread.currentThread();
  4.     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
  5.     ThreadLocalMap map = getMap(t);
  6.     if (map != null) {
  7.         //通过当前的ThreadLocal作为key去获取对应value
  8.         ThreadLocalMap.Entry e = map.getEntry(this);
  9.         if (e != null) {
  10.             //@SuppressWarnings忽略告警的注解
  11.             //"unchecked"表示未经检查的转换相关的警告,通常出现在泛型编程中
  12.             @SuppressWarnings("unchecked")
  13.             T result = (T)e.value;
  14.             return result;
  15.         }
  16.     }
  17.     //threadLocals为空或它的Entry为空时,需要对其进行初始化操作。
  18.     return setInitialValue();
  19. }
复制代码
  1. private T setInitialValue() {
  2.     //初始化为null
  3.     T value = initialValue();
  4.    
  5.     //获取当前线程
  6.     Thread t = Thread.currentThread();
  7.    
  8.     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
  9.     ThreadLocalMap map = getMap(t);
  10.     if (map != null)
  11.         map.set(this, value);
  12.     else
  13.         createMap(t, value);
  14.    
  15.     //返回的其实就是个null
  16.     return value;
  17. }
复制代码
  1. protected T initialValue() {
  2.     return null;
  3. }
复制代码
4. remove 方法源码

核心也是 ThreadLocalMap 中的 remove 方法,会删除 key 对应的 Entry,具体源码后面统一在 ThreadLocalMap 源码中分析。
  1. public void remove() {
  2.     //获取当前线程下的threadLocals对象,对象类型是ThreadLocalMap
  3.     ThreadLocalMap m = getMap(Thread.currentThread());
  4.     if (m != null)
  5.         //通过当前的ThreadLocal作为key调用remove
  6.         m.remove(this);
  7. }
复制代码
5. ThreadLocalMap 源码

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,看了上面的几个源码解释,可以了解到 ThreadLocalMap 其实才是核心。
简单的说,ThreadLocalMap 与 HashMap 类似,如,初始容量 16,一定范围内扩容,Entry 数组存储等,那它与 HashMap 有什么不同呢,下面将对源码进行详解。
ThreadLocalMap 的底层数据结构:

5.1 常量
  1. //初始容量,一定是2的幂等数。
  2. private static final int INITIAL_CAPACITY = 16;
  3. // Entry 数组
  4. private Entry[] table;
  5. //table的长度
  6. private int size = 0;
  7. //扩容阈值
  8. private int threshold;
  9. //设置扩容阈值,长度的 2 / 3
  10. private void setThreshold(int len) {
  11.     threshold = len * 2 / 3;
  12. }
  13. //计算下一个存储位置
  14. private static int nextIndex(int i, int len) {
  15.     return ((i + 1 < len) ? i + 1 : 0);
  16. }
  17. // 计算前一个存储位置
  18. private static int prevIndex(int i, int len) {
  19.     return ((i - 1 >= 0) ? i - 1 : len - 1);
  20. }
复制代码
5.2 Entry 相关源码

由于 Entry 是底层核心源码,所有的操作险些都是围绕着它来进行的,所以关于 Entry 的源码会比力多,我一一拆分进行分析讲解。
静态内部类 Entry

这个是 ThreadLocalMap 的底层数据结构,Entry 数组,每个 Entry 对象,这里的 Entry 继承了 WeakReference,关于弱引用不懂得,可以看我的另一篇文章《Java 引用》
然后将 Entry 的 key 设置承了 弱引用,这有什么作用呢?作用是当 ThreadLocal 失去强引用后,在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉 key,进而 Entry 被内部清算。
  1. //静态内部类Entry
  2. static class Entry extends WeakReference<ThreadLocal<?>> {
  3.     Object value;
  4.     Entry(ThreadLocal<?> k, Object v) {
  5.         // key为弱引用
  6.         super(k);
  7.         value = v;
  8.     }
  9. }
复制代码
获取 Entry

拿到当前线程中对应的 ThreadLocal 所在的 Entry,找不到的话会重新探求,因为当前的 Entry 可能已经扩容,扩容后会重新计算索引位置,详情见扩容机制源码。
源码中的计算索引位置的算法我没有解释,这个我会放在后面解释,涉及到了如何解决 Hash 冲突的题目,这个和我们熟知的 HashMap 是不同的。
  1. //获取Entry
  2. private Entry getEntry(ThreadLocal<?> key) {
  3.     //计算索引位置
  4.     int i = key.threadLocalHashCode & (table.length - 1);
  5.     Entry e = table[i];
  6.     //找到了就返回Entry
  7.     if (e != null && e.get() == key)
  8.             return e;
  9.     else
  10.         //没找到则重新寻找,因为可能发生扩容导致索引重新计算
  11.         return getEntryAfterMiss(key, i, e);
  12. }
  13. //重新获取Entry --从当前索引i的位置向后搜索
  14. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  15.     Entry[] tab = table;
  16.     int len = tab.length;
  17.     //循环遍历,获取对应的 ThreadLocal 所在的 Entry
  18.     while (e != null) {
  19.         //获取Entry对象的弱引用,WeakReference的方法
  20.         ThreadLocal<?> k = e.get();
  21.         if (k == key)
  22.             return e;
  23.         if (k == null)
  24.             //清除无效 Entry,详解见下方
  25.             expungeStaleEntry(i);
  26.         else
  27.             //计算下一个索引位置
  28.             i = nextIndex(i, len);
  29.         
  30.         //可以理解为指针后移
  31.         e = tab[i];
  32.     }
  33.     return null;
  34. }
复制代码
扫除无效 Entry

expunge 删除,抹去,stale 陈旧的,没有用的
第 1 个方法:
根据索引删除对应的桶位,并从给定索引开始,遍历扫除无效的 Entry,何为无效?就是当 Entry 的 key 为 null 时,代表 key 已经被 GC 掉了,对应的 Entry 就无效了。
第 2 个方法:
删除Entry数组中所有无效的Entry,方法中的e.get() == null,代表key被回收了。
第 3 个方法:
扫除一些失效桶位,它执行对数数量的扫描,向后遍历logn个位置,如8,4,2,1。
方法 2、3 最后都通过方法 1 进行桶位的删除。
  1. //根据索引删除对应的桶位
  2. private int expungeStaleEntry(int staleSlot) {
  3.     Entry[] tab = table;
  4.     int len = tab.length;
  5.     //删除该桶位的元素,并将数组长度减1
  6.     tab[staleSlot].value = null;
  7.     tab[staleSlot] = null;
  8.     size--;
  9.     Entry e;
  10.     int i;
  11.     //从当前索引开始,直到当前 Entry为null才会停止遍历
  12.     for (i = nextIndex(staleSlot, len);
  13.          (e = tab[i]) != null;
  14.          i = nextIndex(i, len)) {
  15.         //获取Entry对象的弱引用,WeakReference的方法
  16.         ThreadLocal<?> k = e.get();
  17.         if (k == null) {//说明key已失效
  18.             //删除该桶位的元素,并将数组长度减1
  19.             e.value = null;
  20.             tab[i] = null;
  21.             size--;
  22.         } else {//说明key有效,需要将其Rehash
  23.             //计算rehash后索引位置
  24.             int h = k.threadLocalHashCode & (len - 1);
  25.             if (h != i) {
  26.                 tab[i] = null;
  27.                 //移动元素位置,若rehash后索引位置有其他元素,则继续向后移动,直至为空
  28.                 while (tab[h] != null)
  29.                     h = nextIndex(h, len);
  30.                 tab[h] = e;
  31.             }
  32.         }
  33.     }
  34.     //直到当前 Entry为null才会停止遍历,i为其索引
  35.     return i;
  36. }
  37. //删除Entry数组中所有无效的Entry,用于rehash时
  38. private void expungeStaleEntries() {
  39.     Entry[] tab = table;
  40.     int len = tab.length;
  41.     for (int j = 0; j < len; j++) {
  42.         Entry e = tab[j];
  43.         //获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了
  44.         if (e != null && e.get() == null)
  45.             //根据索引删除对应的桶位
  46.             expungeStaleEntry(j);
  47.     }
  48. }
  49. //清楚一些清除桶位,它执行对数数量的扫描
  50. private boolean cleanSomeSlots(int i, int n) {
  51.     boolean removed = false;
  52.     Entry[] tab = table;
  53.     int len = tab.length;
  54.     //向后遍历logn个位置,如8,4,2,1
  55.     do {
  56.         i = nextIndex(i, len);
  57.         Entry e = tab[i];
  58.         //获取Entry对象的弱引用,Entry不为空而弱引用为空,代表被GC了
  59.         if (e != null && e.get() == null) {
  60.             n = len;
  61.             removed = true;
  62.             //根据索引删除对应的桶位
  63.             i = expungeStaleEntry(i);
  64.         }
  65.     } while ( (n >>>= 1) != 0);//对数递减
  66.     return removed;
  67. }
复制代码
更换无效 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 指向。
  1. private void replaceStaleEntry(ThreadLocal<?> key, Object value,
  2.                                int staleSlot) {
  3.     Entry[] tab = table;
  4.     int len = tab.length;
  5.     Entry e;
  6.     //记录失效元素的索引
  7.     int slotToExpunge = staleSlot;
  8.     //从失效元素位置向前遍历,直到当前 Entry为null才会停止遍历
  9.     for (int i = prevIndex(staleSlot, len);
  10.          (e = tab[i]) != null;
  11.          i = prevIndex(i, len))
  12.         if (e.get() == null)
  13.             //更新失效元素的索引,目的是找第一个失效的元素
  14.             slotToExpunge = i;
  15.     //从失效元素向后遍历
  16.     for (int i = nextIndex(staleSlot, len);
  17.          (e = tab[i]) != null;
  18.          i = nextIndex(i, len)) {
  19.         ThreadLocal<?> k = e.get();
  20.         //找到了对应key
  21.         if (k == key) {
  22.             //更新该位置的value
  23.             e.value = value;
  24.             //把失效元素换到当前位置
  25.             tab[i] = tab[staleSlot];
  26.             //把当前Entry移动到失效元素位置
  27.             tab[staleSlot] = e;
  28.             
  29.             //slotToExpunge是第一个失效元素的索引,若条件成立,向前没有失效元素
  30.             if (slotToExpunge == staleSlot)
  31.                 //从当前索引开始,清理失效元素
  32.                 slotToExpunge = i;
  33.             
  34.             // 清理失效元素,详情见清除无效Entry相关源码
  35.             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  36.             return;
  37.         }
  38.         
  39.         //代表向前遍历没有找到第一个失效元素的位置
  40.         if (k == null && slotToExpunge == staleSlot)
  41.             //所以条件成立的i是向后遍历的的第一个失效元素的位置
  42.             slotToExpunge = i;
  43.     }
  44.    
  45.     //没找到key,则在失效元素索引的位置,新建Entry
  46.     tab[staleSlot].value = null;
  47.     tab[staleSlot] = new Entry(key, value);
  48.    
  49.     // 条件成立说明在找到了staleSlot前面找到了其他的失效元素
  50.     if (slotToExpunge != staleSlot)
  51.         
  52.         // 清理失效元素,详情见清除无效Entry相关源码
  53.         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  54. }
复制代码
5.3 构造方法

另有一个基于 parentMap 的构造方法,由于目前仅在创建 InheritableThreadLocal 时调用,关于它这里不具体睁开,后续会针对该类进行详解。
  1. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  2.     // 初始化数组
  3.     table = new Entry[INITIAL_CAPACITY];
  4.     //计算存储位置
  5.     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  6.     //存储元素,并将size设置为1
  7.     table[i] = new Entry(firstKey, firstValue);
  8.     size = 1;
  9.     //设置扩容阈值
  10.     setThreshold(INITIAL_CAPACITY);
  11. }
复制代码
5.4 set 方法源码

设置 key,vlaue,key 就是 ThreadLocal 对象。
  1. private void set(ThreadLocal<?> key, Object value) {
  2.     Entry[] tab = table;
  3.     int len = tab.length;
  4.     //计算索引位置
  5.     int i = key.threadLocalHashCode & (len-1);
  6.     //从当前索引开始,直到当前Entry为null才会停止遍历
  7.     for (Entry e = tab[i];
  8.          e != null;
  9.          e = tab[i = nextIndex(i, len)]) {
  10.         ThreadLocal<?> k = e.get();
  11.         //如果key存在且等于当前key,代表之前存在的,直接覆盖
  12.         if (k == key) {
  13.             e.value = value;
  14.             return;
  15.         }
  16.         //如果key不存在,说明已失效,需要替换,详情见替换无效Entry源码
  17.         if (k == null) {
  18.             replaceStaleEntry(key, value, i);
  19.             return;
  20.         }
  21.     }
  22.     //没有key则新建一个Entry即可
  23.     tab[i] = new Entry(key, value);
  24.     int sz = ++size;
  25.     //清理一些失效元素,若清理失败且达到常量中的扩容阈值,则进行rehash操作
  26.     if (!cleanSomeSlots(i, sz) && sz >= threshold)
  27.         rehash();
  28. }
  29. //删除Entry数组中所有无效的Entry并扩容
  30. private void rehash() {
  31.     //删除Entry数组中所有无效的Entry
  32.     expungeStaleEntries();
  33.     if (size >= threshold - threshold / 4)
  34.         //扩容,详情见下面的扩容机制源码
  35.         resize();
  36. }
复制代码
5.5 remove 方法源码

删除key对应的entry
  1. private void remove(ThreadLocal<?> key) {
  2.     Entry[] tab = table;
  3.     int len = tab.length;
  4.     //计算存储位置
  5.     int i = key.threadLocalHashCode & (len-1);
  6.    
  7.     //从当前索引开始,直到当前Entry为null才会停止遍历
  8.     for (Entry e = tab[i];
  9.          e != null;
  10.          e = tab[i = nextIndex(i, len)]) {
  11.         if (e.get() == key) {
  12.             //清除该对象的强引用,下次在通过get方法获取引用则返回null
  13.             e.clear();
  14.             //清除无效元素
  15.             expungeStaleEntry(i);
  16.             return;
  17.         }
  18.     }
  19. }
复制代码
5.6 扩容机制源码

将元素转移到新的Entry 数组,长度是原来的两倍。
  1. private void resize() {
  2.     //创建原数组长度两倍的新数组
  3.     Entry[] oldTab = table;
  4.     int oldLen = oldTab.length;
  5.     int newLen = oldLen * 2;
  6.     Entry[] newTab = new Entry[newLen];
  7.     int count = 0;        //计算当前元素数量
  8.     for (int j = 0; j < oldLen; ++j) {
  9.         Entry e = oldTab[j];
  10.         if (e != null) {
  11.             ThreadLocal<?> k = e.get();
  12.             if (k == null) {        //key失效则值也顺便设为null
  13.                 e.value = null;         // Help the GC
  14.             } else {
  15.                 //重新计算索引位置
  16.                 int h = k.threadLocalHashCode & (newLen - 1);
  17.                 //移动元素位置,若rehash后索引位置有其他元素,则继续向后移动,直至为空
  18.                 while (newTab[h] != null)
  19.                     h = nextIndex(h, newLen);
  20.                 newTab[h] = e;
  21.                 count++;
  22.             }
  23.         }
  24.     }
  25.     setThreshold(newLen);
  26.     size = count;
  27.     table = newTab;
  28. }
复制代码
四、ThreadLocalMap 的 Hash 冲突

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

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

2. 开放地址法

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

简单说,就是一旦发生了冲突,就去探测探求下一个空的散列地址,根据上面的源码也能大抵了解该处理方式。
源码中的公式是key.threadLocalHashCode & (length - 1)。
公式类似 HashMap 的寻址算法,详情见HashMap源码,由于数组长度是 2 的 n 次幂,所以这里的与运算就是取模,得到索引 i,这样做是为了分布更匀称,减少冲突产生。
threadLocalHashCode 源码如下:
  1. private final int threadLocalHashCode = nextHashCode();
  2. //初始化线程安全的Integer
  3. private static AtomicInteger nextHashCode =
  4.     new AtomicInteger();
  5. //斐波那契散列乘数 --结果分布更均匀
  6. private static final int HASH_INCREMENT = 0x61c88647;
  7. //自增返回下一个hash code
  8. private static int nextHashCode() {
  9.    
  10.     return nextHashCode.getAndAdd(HASH_INCREMENT);
  11. }
复制代码
线性探测法的缺点:
五、留意事项

1. 关于内存泄漏

在了解了 ThreadLocal 的内部实现以后,我们知道了数据其实存储在 ThreadLocalMap 中。这就意味着,线程只要不退出,则引用一直存在。
当线程退出时,Thread 类会对一些资源进行清算,此中就有threadLocals,源码如下:
  1. private void exit() {
  2.     if (group != null) {
  3.         group.threadTerminated(this);
  4.         group = null;
  5.     }
  6.     target = null;
  7.     //加速一些资源的清理
  8.     threadLocals = null;
  9.     inheritableThreadLocals = null;
  10.     inheritedAccessControlContext = null;
  11.     blocker = null;
  12.     uncaughtExceptionHandler = null;
  13. }
复制代码
因此,当使用的线程一直没有退出(如使用线程池),这时如果将一些大对象放入 ThreadLocal 中,且没有及时清算,就可能会出现内存泄漏的风险
所以我们要养成习惯每次使用完 ThreadLocal 都要调用 remove 方法进行清算。
2. 关于数据混乱

通过对内存泄漏的解释,我们了解了当使用的线程一直没有退出,而又没有纵然清算 ThreadLocal,则此中的数据会一直存在。
这除了内存泄漏另有什么题目呢?我们在开发过程中,请求一样平常都是通过 Tomcat 处理,而其在处理请求时采用的就是线程池。
这就意味着请求线程被 Tomcat 回收后,不一定会立刻烧毁,如果不在请求结束后主动 remove 线程中的 ThreadLocal 信息,可能会影响后续逻辑,拿到脏数据。
我在开发过程中就碰到了这个题目,详情见ThreadLocal中的用户信息混乱题目。所以无论如何,在每次使用完 ThreadLocal 都要调用 remove 方法进行清算。
3. 关于继承性

同一个 ThreadLocal 变量,在父线程中被设置值后,在子线程其实是获取不到的。通过源码我们也知道,我们操作的都是当前线程下的 ThreadLocalMap ,所以这其实是正常的。
测试代码如下:
  1. public class FuXing {
  2.     /**
  3.      * 初始化ThreadLocal
  4.      */
  5.     private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
  6.     public static void main (String[] args) {
  7.         myThreadLocal.set("father thread");
  8.         System.out.println(myThreadLocal.get());         //father thread
  9.         new Thread(()->{
  10.             System.out.println(myThreadLocal.get());        //null
  11.         },"thread 1").start();
  12.     }
  13. }
复制代码
那么这可能会导致什么题目呢?比如我们在本服务调用外部服务,大概本服务开启新线程去进行异步操作,此中都无法获取 ThreadLocal 中的值。
虽然都有其他解决方法,但是有没有让子线程也能直接获取到父线程的 ThreadLocal 中的值呢?这就用到了 InheritableThreadLocal。
  1. public class FuXing {
  2.     /**
  3.      * 初始化ThreadLocal
  4.      */
  5.     private static final InheritableThreadLocal<String> myThreadLocal
  6.             = new InheritableThreadLocal<>();
  7.     public static void main (String[] args) {
  8.         myThreadLocal.set("father thread");
  9.         System.out.println(myThreadLocal.get());         //father thread
  10.         new Thread(()->{
  11.             System.out.println(myThreadLocal.get());        //father thread
  12.         },"thread 1").start();
  13.     }
  14. }
复制代码
InheritableThreadLocal 就是继承了 ThreadLocal,在创建和获取变量实例 inheritableThreadLocals 而不再是threadLocals,源码如下。
  1. public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  2.     protected T childValue(T parentValue) {
  3.         return parentValue;
  4.     }
  5.     ThreadLocalMap getMap(Thread t) {
  6.        return t.inheritableThreadLocals;
  7.     }
  8.     void createMap(Thread t, T firstValue) {
  9.         t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  10.     }
  11. }
复制代码
总结

本文主要讲述了 ThreadLocal 的使用以及对其源码进行了详解,了解了 ThreadLocal 可以线程隔离的缘故起因。通过对 ThreadLocalMap 的分析,知道了其底层数据结构和如何解决 Hash 冲突的。
最后通过对 ThreadLocal 特点的分析,了解到有哪些需要留意的点,制止以后开发过程中碰到类似题目,若发现其他题目欢迎指正交流。
参考:
[1] 翟连续/薛宾田. Java并发编程之美.
[2] 葛一鸣/郭超. 实战Java高并发程序设计.
[3] 靳宇栋. Hello 算法.

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4