千家信息网

ThreadLocal源码分析之入如何实现ThreadLocal

发表于:2024-11-26 作者:千家信息网编辑
千家信息网最后更新 2024年11月26日,本篇内容主要讲解"ThreadLocal源码分析之入如何实现ThreadLocal",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"ThreadLocal源码
千家信息网最后更新 2024年11月26日ThreadLocal源码分析之入如何实现ThreadLocal

本篇内容主要讲解"ThreadLocal源码分析之入如何实现ThreadLocal",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"ThreadLocal源码分析之入如何实现ThreadLocal"吧!

1 简介

ThreadLocal是线程本地变量(缓存),其往往用来实现在同一线程内部的变量进行交互的情景,不存在线程之间的交互。其对每一个线程内部都维护了一个数据,在a线程set的值,也只能在a线程里进行get。

具体的使用场景:比如可以用ThreadLocal来封装数据库连接;也可以在复杂逻辑下,用ThreadLocal来作为方法之间的数据传递:如果一开始设置了一个数据,但因为调用逻辑复杂,跨越了很多的类和方法后,需要再次获取到这个数据,那么这个时候就可以用ThreadLocal来对这个数据进行缓存,访问就很方便了。但需要注意的是,这一切操作都必须保证是在同一个线程中进行的,也就是在同一个线程中进行set,也在同一个线程中进行get。在Spring框架中除了使用HashMap和ConcurrentHashMap来做各种缓存之外,也会用到ThreadLocal来缓存数据。

1.1 ThreadLocal与同步控制

初学ThreadLocal可能会认为它跟synchronized和ReentrantLock是同样的同步机制(我之前也写过对ReentrantLock进行源码分析的文章 AQS源码深入分析之独占模式-ReentrantLock锁特性详解),但实际上他们完全是两种东西。实现的方式不同,解决的场景也不同。synchronized和ReentrantLock使用的是时间换空间的思路,是用来在多线程场景中访问共享变量用的。获取不到资源的线程会进行排队,等待去获取;而ThreadLocal使用空间换时间的做法,是用来做数据隔离和单个线程内的数据共享用的。set方法只会在当前线程中缓存数据,而get方法也只会在当前线程中获取到这个数据,在别的线程中调用get方法是获取不到的(在别的线程中调用get方法只会获取到ThreadLocal在那个线程中缓存的值)。

1.2 ThreadLocalMap

ThreadLocal内部维护了一个静态内部类ThreadLocalMap,ThreadLocal内部的所有操作都是在其中实现的(另外提一嘴:我看过很多讲ThreadLocal源码分析的文章,但他们都没有分析ThreadLocalMap的实现。没有分析到ThreadLocalMap这个层面,当然会觉得ThreadLocal的实现很简单。因为ThreadLocal只能说是个包装,核心的操作都在ThreadLocalMap里面):

 1 static class ThreadLocalMap {   2   3     static class Entry extends WeakReference> {   4         /**   5          * The value associated with this ThreadLocal.   6          */   7         Object value;   8   9         Entry(ThreadLocal k, Object v) {  10             super(k);  11             value = v;  12         }  13     }  14  15     //...  16  17     /**  18      * The table, resized as necessary.  19      * table.length MUST always be a power of two.  20      */  21     private Entry[] table;  22  23     //...  24 }

可以看到其中也维护了一个Entry数组table,而Entry是ThreadLocalMap中的静态内部类,并且Entry类本质上是一个包装ThreadLocal的弱引用,其内部维护着一个强引用的value属性,存放的便是ThreadLocal中需要存储的值。

而ThreadLocalMap是放到了Thread类的内部属性中,由ThreadLocal来进行维护:

1 public class Thread implements Runnable {  2     //...  3  4     /* ThreadLocal values pertaining to this thread. This map is maintained  5      * by the ThreadLocal class. */  6     ThreadLocal.ThreadLocalMap threadLocals = null;  7  8     //...  9 }

也就是说每个线程中都会有一份缓存副本,每个线程可以访问自己内部的副本,而该副本对于其他线程来说是隔离的。 **总结来说,在同一个线程中的多个ThreadLocal,会通过hash算法放入到当前线程中的threadLocals属性中的table数组中,也就是哈希槽。**只不过不同于HashMap遇到hash冲突就采用挂链表的方式,ThreadLocal采用的是 **线性探测(开放地址)**的方式来放入的。

ThreadLocal内部会维护着一条从 Thread的引用->Thread->ThreadLocalMap->Entry->Entry的值的引用链,如下图所示:

其上的虚线为弱引用,实线为强引用。当使用完Thread对象后需要被回收时,在下一次gc的时候,因为Entry连着的ThreadLocal引用是弱引用,所以Thread对象能够顺利被回收掉。而如果是强引用,并且ThreadLocal是用static修饰的话,可能就不会被回收,从而产生内存泄漏的问题。

1.3 ThreadLocal的内存泄露问题

虽然上面使用了弱引用,以此来保证Thread对象能够成功被回收掉。但是正如上面ThreadLocalMap的源码所示,Entry其中的value仍为强引用。所以如果使用不当的话,仍然会造成内存泄露的问题出现。考虑这么一种情况:如果使用完ThreadLocal、同时其引用断开,并且没有其他的强引用,则在下一次gc的时候,这个ThreadLocal对象就会被回收,变为null。但是因为Entry中的value是强引用,所以此时在threadLocals的Entry数组中就会有一个ThreadLocal为null,但是其中的value仍然有值的Entry。此时就再也不能通过原来的ThreadLocal(此时为null)来访问到该value了。而该线程却一直存在(比如说是线程池),Thread又强引用着ThreadLocalMap,因此ThreadLocalMap也不会被回收。于是就产生了Entry中value的内存泄露。

解决办法是再一次调用get/set/remove方法,这三个方法在其实现的内部逻辑中都会遍历删除Entry为null的值,以此来避免内存泄漏的发生(get和set方法只能删除部分垃圾数据,而且可能在还没有遍历到ThreadLocal为null的Entry时,这两个方法就已经提前成功返回了。所以最好是使用remove方法。在使用完ThreadLocal后显式调用一次remove方法,这将会把当前这个ThreadLocal对应的Entry删除掉,从根本上杜绝了内存泄漏的发生。在后面的源码分析中可以看到这点)。

进一步思考:如果将value也置为弱引用行不行?如果这么做的话,value除了Entry这个弱引用之外,就再没有别的引用了。这样的话在下一次gc时value值就会被清除掉,而Thread对象却一直存活者,再次调用就会返回null。这是绝对不能容忍的,因为这个时候就不是内存泄不泄漏的问题了,此时就变成了程序的bug。

1.4 SimpleDateFormat的线程安全问题

这是《阿里巴巴编码规范》中的一条。SimpleDateFormat因为其内部的Calendar属性而存在线程安全问题,如果把其定义成static的成员变量,多个线程同时更改获取它,就可能会出现问题。解决方法是使用Java 8中新添加的时间API,或者使用第三方的时间类库(Joda-Time)。当然使用ThreadLocal来包装SimpleDateFormat的方式也不失为一种好的解决办法:

1 private static final ThreadLocal SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));  2  3 public static Date parse(String str) throws ParseException {  4     return SDF.get().parse(str);  5 }  6  7 public static String format(Date date) {  8     return SDF.get().format(date);  9 }

2 构造器

1 /**  2  * ThreadLocal:  3  */  4 public ThreadLocal() {  5     //空实现  6 }

虽然ThreadLocal的构造器是空实现,但是同时会完成hashCode的计算,如下所示:

1 public class ThreadLocal {   2   3     private final int threadLocalHashCode = nextHashCode();   4   5     private static AtomicInteger nextHashCode =   6             new AtomicInteger();   7   8     private static final int HASH_INCREMENT = 0x61c88647;   9  10     private static int nextHashCode() {  11         /*  12         nextHashCode是AtomicInteger类型的,这里是在获取当前nextHashCode的值,然后会加上  13         HASH_INCREMENT。注意这里的nextHashCode是static修饰的,也就是类变量。也就是说,  14         每调用一次ThreadLocal的构造器,就会生成一个不一样的hashCode  15          */  16         return nextHashCode.getAndAdd(HASH_INCREMENT);  17     }  18  19     //...  20 }

不同于HashMap中计算hashCode是采用key的hashCode方法高低16位异或的方式,在ThreadLocal中计算hashCode是使用了一个固定的值0x61c88647,那么为什么会是这个数呢?其实0x61c88647换算成十进制就是1640531527。而Java中的int是32位,也就是2654435769 = -1640531527。这里的 , 其 中的φ也就是所谓的黄金分割率,也就是近似于0.618的那个数。 而黄金比例又与斐波那契数( )之间有密切关系,使用这个数时会使得 散列的结果很均匀:

3 set方法

  1 /**    2  * ThreadLocal:    3  */    4 public void set(T value) {    5     //获取当前的线程    6     Thread t = Thread.currentThread();    7     //获取线程中的threadLocals    8     ThreadLocalMap map = getMap(t);    9     if (map != null)   10         //如果threadLocals存在,就往其中存放数据(this代表当前的ThreadLocal)   11         map.set(this, value);   12     else   13         //否则就完成ThreadLocalMap的初始化并放入数据   14         createMap(t, value);   15 }   16   17 /**   18  * 第8行代码处:   19  */   20 ThreadLocalMap getMap(Thread t) {   21     //返回上面说过的Thread中的threadLocals,也就是当前线程的缓存副本   22     return t.threadLocals;   23 }

4 createMap方法

在上面第14行代码处可以看到,如果ThreadLocalMap没有初始化,就进行初始化并存数据的操作:

 1 /**   2  * ThreadLocal:   3  * 首先来看一下初始化的过程   4  */   5 void createMap(Thread t, T firstValue) {   6     /*   7     前面看到在ThreadLocal的构造器中是空实现,而在set方法中的此处完成ThreadLocalMap的延迟初始化,   8     初始化完成后赋值给当前线程的threadLocals属性   9      */  10     t.threadLocals = new ThreadLocalMap(this, firstValue);  11 }  12  13 /**  14  * ThreadLocalMap构造器  15  */  16 ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {  17     //对Entry数组table进行初始化,初始容量INITIAL_CAPACITY为16  18     table = new Entry[INITIAL_CAPACITY];  19     //获取哈希槽的位置。这里的按位与也就是在做threadLocalHashCode对数组容量取余的结果  20     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  21     //对哈希槽做初始化(只初始化当前槽的位置,也体现了延迟初始化的思想)  22     table[i] = new Entry(firstKey, firstValue);  23     //计数为1  24     size = 1;  25     //设置threshold  26     setThreshold(INITIAL_CAPACITY);  27 }  28  29 /**  30  * 第22行代码处:  31  */  32 Entry(ThreadLocal k, Object v) {  33     //这里会将当前ThreadLocal放入到弱引用中  34     super(k);  35     //这里会将要存入ThreadLocal的值存入到Entry的value中  36     value = v;  37 }  38  39 /**  40  * 第26行代码处:  41  * 设置threshold值(threshold相当于HashMap中负载因子的概念),可以看到这里的策略是数组容量的2/3  42  */  43 private void setThreshold(int len) {  44     threshold = len * 2 / 3;  45 }

5 map.set方法

在set方法第11行代码处可以看到,如果ThreadLocalMap已经初始化了,就往其中插入数据:

1  /**   2  * ThreadLocal:   3  * 再来看一下set方法中、如果当前线程中的ThreadLocalMap已经初始化了,就会执行本方法完成set操作   4  */   5 private void set(ThreadLocal key, Object value) {   6   7     Entry[] tab = table;   8     int len = tab.length;   9     //和上面ThreadLocalMap构造器中的一样,这里是在获取哈希槽的位置  10     int i = key.threadLocalHashCode & (len - 1);  11  12     for (Entry e = tab[i];  13         e != null;  14         e = tab[i = nextIndex(i, len)]) {  15         //获取当前槽中的Entry中保存的ThreadLocal  16         ThreadLocal k = e.get();  17  18         /*  19         如果这个ThreadLocal就是当前线程的ThreadLocal的话,就更新一下value值,也就是做值的覆盖。然后返回  20         如果不等,就说明发生了hash冲突,此时继续寻找下一个哈希槽,也就是用线性探测的方式  21          */  22         if (k == key) {  23             e.value = value;  24             return;  25         }  26  27         /*  28         如果当前槽不为null,但是其中保存的ThreadLocal为null,说明这个弱引用被回收了(上面说过Entry继承弱引用)  29         此时会删除这个位置的垃圾数据(以及其他无效的位置),防止内存泄漏  30          */  31         if (k == null) {  32             replaceStaleEntry(key, value, i);  33             return;  34         }  35     }  36  37     /*  38     走到这里说明上面的for循环全走完了也没有找到key相等的节点或key是null的节点,此时的i就是下一个要插入的槽位  39     那么现在就把新的value数据插入到这个位置中就行了  40      */  41     tab[i] = new Entry(key, value);  42     //计数+1  43     int sz = ++size;  44     /*  45     当存放完数据后,此时会查看是否需要清理垃圾数据,如果没有垃圾数据的话,  46     并且当前数组的容量大于等于threshold阈值,就进行"扩容"  47      */  48     if (!cleanSomeSlots(i, sz) && sz >= threshold)  49         rehash();  50 }

6 清理脏Entry方法

之前已经说过ThreadLocal会存在内存泄漏的问题,也就是存在 ThreadLocal为null的脏数据。所以在上面第32行代码处可以看到,对其进行了处理:

  1 /**    2  * ThreadLocal:    3  */    4 private void replaceStaleEntry(ThreadLocal key, Object value,    5                                int staleSlot) {    6     Entry[] tab = table;    7     int len = tab.length;    8     Entry e;    9   10     /*   11     从staleSlot位置起往前寻找第一个ThreadLocal为null的哈希槽位置(也就是垃圾数据)   12     这里没有直接用staleSlot而是用了一次遍历确定下来的slotToExpunge是因为:这里不光会   13     删除staleSlot位置的垃圾数据,还会把所有的垃圾数据都删除   14      */   15     int slotToExpunge = staleSlot;   16     //向前环形搜索垃圾数据   17     for (int i = prevIndex(staleSlot, len);   18          (e = tab[i]) != null;   19          i = prevIndex(i, len))   20         if (e.get() == null)   21             slotToExpunge = i;   22   23     //向后环形搜索   24     for (int i = nextIndex(staleSlot, len);   25          (e = tab[i]) != null;   26          i = nextIndex(i, len)) {   27         //获取当前槽中的Entry中保存的ThreadLocal   28         ThreadLocal k = e.get();   29   30         //如果key(ThreadLocal)相同的话   31         if (k == key) {   32             //就做一下值的覆盖   33             e.value = value;   34   35             /*   36             同时会将staleSlot(垃圾数据位置处)和i位置的数据做一下交换   37             (因为tab[i]的数据已经在上面缓存了,所以不需要暂存变量)   38             交换之后,垃圾数据就到了i处(这样就可以保证垃圾数据最后会放在   39             相同key的最后一个哈希槽位置处,保证了hash的顺序)   40              */   41             tab[i] = tab[staleSlot];   42             tab[staleSlot] = e;   43   44             /*   45             如果在之前的向前环形搜索的过程中没有找到垃圾数据的话,就把i赋值给slotToExpunge,   46             意思是以当前i位置处作为清理的起点   47              */   48             if (slotToExpunge == staleSlot)   49                 slotToExpunge = i;   50             //找到垃圾数据并进行清理   51             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);   52             return;   53         }   54   55         /*   56         如果在之前的向前环形搜索的过程中没有找到垃圾数据,但是在此时的向后环形搜索的过程中找到了,   57         就一样把i赋值为slotToExpunge,以当前i位置处作为清理的起点   58          */   59         if (k == null && slotToExpunge == staleSlot)   60             slotToExpunge = i;   61     }   62   63     //如果在上面的循环中没有找到相同的key的话,此时将垃圾数据位置处的value置为null,去掉强引用   64     tab[staleSlot].value = null;   65     //同时将一个新的Entry赋值进去(也就是当前需要set进去的数据)   66     tab[staleSlot] = new Entry(key, value);   67   68     //如果在上面向后环形搜索的过程中找到了垃圾数据的话,就一样需要进行清理   69     if (slotToExpunge != staleSlot)   70         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);   71 }   72   73 /**   74  * 第17行和第19行代码处:   75  * 根据指定的哈希槽位置查找上一个位置处,如果已经到第一个位置了,就重新返回最后一个位置处   76  */   77 private static int prevIndex(int i, int len) {   78     return ((i - 1 >= 0) ? i - 1 : len - 1);   79 }   80   81 /**   82  * 第24行、第26行、第105行、第107行、第132行和第154行代码处:   83  * 根据指定的哈希槽位置查找下一个位置处,如果已经到最后一个位置了,就重新返回第一个位置处   84  */   85 private static int nextIndex(int i, int len) {   86     return ((i + 1 < len) ? i + 1 : 0);   87 }   88   89 /**   90  * 第51行、第70行和第164行代码处:   91  */   92 private int expungeStaleEntry(int staleSlot) {   93     Entry[] tab = table;   94     int len = tab.length;   95   96     //删除垃圾数据位置处的强引用value和Entry   97     tab[staleSlot].value = null;   98     tab[staleSlot] = null;   99     //计数-1  100     size--;  101  102     Entry e;  103     int i;  104     //从staleSlot位置处向后环形搜索  105     for (i = nextIndex(staleSlot, len);  106          (e = tab[i]) != null;  107          i = nextIndex(i, len)) {  108         //获取当前槽中的Entry中保存的ThreadLocal  109         ThreadLocal k = e.get();  110         if (k == null) {  111             //如果再次遇到垃圾数据,就将其清理掉,并且计数-1  112             e.value = null;  113             tab[i] = null;  114             size--;  115         } else {  116             /*  117             在线性探测中删除一个节点的话,是不能简单地将一个节点置为null就完事了的。因为在线性探测查找的时候,  118             如果遍历时遇到一个为null的位置,就可以停止遍历、认定为找不到这个数据了。而如果删除的时候只将这个数据  119             置为null的话,那么后面的节点就有可能会访问不到。本来存在的数据,但却访问不到,从而出现了问题  120             所以在这里需要对staleSlot后面的节点做一些处理,这里选择的是rehash的方式  121  122             获取哈希槽的位置  123              */  124             int h = k.threadLocalHashCode & (len - 1);  125             //如果这次获取哈希槽的位置和i不同的话(如果相同就不转移)  126             if (h != i) {  127                 //就将tab[i]位置的数据清空  128                 tab[i] = null;  129  130                 //并且重新线性探测一个新的空位置处  131                 while (tab[h] != null)  132                     h = nextIndex(h, len);  133                 //同时把数据转移进去  134                 tab[h] = e;  135             }  136         }  137     }  138     /*  139     返回staleSlot位置后第一个为null的哈希槽位置,从本方法的实现中可以看出:本方法只是清理了从staleSlot  140     到其后不为null的这一段哈希槽的垃圾数据,并不是清理全部哈希槽。清理全部的话需要借助下面的cleanSomeSlots方法  141      */  142     return i;  143  144  145 /**  146  * 第51行和第70行代码处:  147  */  148 private boolean cleanSomeSlots(int i, int n) {  149     boolean removed = false;  150     Entry[] tab = table;  151     int len = tab.length;  152     do {  153         //获取下一个哈希槽的位置  154         i = nextIndex(i, len);  155         //获取其中的Entry  156         Entry e = tab[i];  157         //如果有垃圾数据的话(Entry不为null但是其中的ThreadLocal为null)  158         if (e != null && e.get() == null) {  159             //就将n重新置为当前数组的长度,再重新进行do-while循环  160             n = len;  161             //删除标志位置为true  162             removed = true;  163             //调用expungeStaleEntry方法来删除垃圾数据,删除后会继续循环,直到n等于0为止  164             i = expungeStaleEntry(i);  165         }  166     //n每次循环都会除以2  167     } while ((n >>>= 1) != 0);  168     //返回是否删除过垃圾数据的标志位  169     return removed;  170 }

7 扩容方法

在map.set方法第49行代码处可以看到 ,rehash方法会判断是否需要扩容,同时会清除掉可能存在的垃圾数据:

1  /**   2  * ThreadLocal:   3  * 是否需要扩容首先还会查看当前全表是否含有垃圾数据,如果有垃圾数据并且删除后还是比数组容量的一半多的话,才进行扩容   4  */   5 private void rehash() {   6     //全表扫描是否含有垃圾数据,如果有的话就进行删除   7     expungeStaleEntries();   8   9     /*  10     如果调用上面expungeStaleEntries方法完毕后,size还是大于等于threshold*0.75(也就是数组容量的一半),  11     就会进行扩容操作  12      */  13     if (size >= threshold - threshold / 4)  14         resize();  15 }  16  17 /**  18  * 第7行代码处:  19  * 从table数组的第一个位置处向后遍历,如果发现有垃圾数据的话(Entry不为null但是其中的ThreadLocal为null),  20  * 就调用expungeStaleEntry方法进行删除  21  */  22 private void expungeStaleEntries() {  23     Entry[] tab = table;  24     int len = tab.length;  25     for (int j = 0; j < len; j++) {  26         Entry e = tab[j];  27         if (e != null && e.get() == null)  28             expungeStaleEntry(j);  29     }  30 }  31  32 /**  33  * 第14行代码处:  34  */  35 private void resize() {  36     Entry[] oldTab = table;  37     int oldLen = oldTab.length;  38     //新数组的容量为旧数组的两倍(这里没有使用<<1的方式感觉是因为历史遗留代码的原因)  39     int newLen = oldLen * 2;  40     //创建新数组  41     Entry[] newTab = new Entry[newLen];  42     int count = 0;  43  44     //遍历旧数组上的每一个哈希槽位  45     for (int j = 0; j < oldLen; ++j) {  46         Entry e = oldTab[j];  47         if (e != null) {  48             ThreadLocal k = e.get();  49             if (k == null) {  50                 /*  51                 如果Entry不为null但是其中的ThreadLocal为null,就说明当前这个是垃圾数据  52                 此时将它的value也置为null,便于GC回收(这里的删除就不需要考虑后续节点的  53                 rehash了,因为所有的节点最后都是要转移到新数组的)  54                  */  55                 e.value = null;  56             } else {  57                 //根据新数组容量来进行hash  58                 int h = k.threadLocalHashCode & (newLen - 1);  59                 //通过线性探测的方式来找到要插入的位置  60                 while (newTab[h] != null)  61                     h = nextIndex(h, newLen);  62                 //并且把数据转移进去  63                 newTab[h] = e;  64                 //新数组计数+1  65                 count++;  66             }  67         }  68     }  69  70     //走到这里说明完成了数据迁移的过程,此时重新计算一下threshold值  71     setThreshold(newLen);  72     //更新一下size  73     size = count;  74     //将扩容后新的数组赋值给table  75     table = newTab;  76 }

8 get方法

 1 /**   2  * ThreadLocal:   3  */   4 public T get() {   5     //获取当前的线程   6     Thread t = Thread.currentThread();   7     //获取线程中的threadLocals   8     ThreadLocalMap map = getMap(t);   9     //如果ThreadLocalMap已经初始化了的话  10     if (map != null) {  11         //从threadLocals中寻找对应的Entry  12         Entry e = map.getEntry(this);  13         //如果找到的话  14         if (e != null) {  15             //直接返回Entry中的value就行了  16             @SuppressWarnings("unchecked")  17             T result = (T) e.value;  18             return result;  19         }  20     }  21     //如果ThreadLocalMap没有初始化,或者没找到Entry的话,就在setInitialValue方法中进行处理  22     return setInitialValue();  23 }  24  25 /**  26  * 第12行代码处:  27  */  28 private Entry getEntry(ThreadLocal key) {  29     //获取哈希槽的位置  30     int i = key.threadLocalHashCode & (table.length - 1);  31     //获取Entry  32     Entry e = table[i];  33     if (e != null && e.get() == key)  34         //如果Entry不为null,并且其中的ThreadLocal也相等的话,就说明找到了,返回这个Entry  35         return e;  36     else  37         //否则未命中的话,就在getEntryAfterMiss方法中进行处理  38         return getEntryAfterMiss(key, i, e);  39 }  40  41 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {  42     Entry[] tab = table;  43     int len = tab.length;  44  45     while (e != null) {  46         ThreadLocal k = e.get();  47         if (k == key)  48             //如果ThreadLocal相等的话,就返回这个Entry  49             return e;  50         if (k == null)  51             /*  52             如果Entry不为null但是其中的ThreadLocal为null,就说明当前这个是垃圾数据  53             调用expungeStaleEntry方法来进行删除  54              */  55             expungeStaleEntry(i);  56         else  57             //否则通过线性探测的方式来找到下一个哈希槽的位置  58             i = nextIndex(i, len);  59         //重新更新一下Entry的指向  60         e = tab[i];  61     }  62     //如果循环走完还找不到的话,就返回null  63     return null;  64 }  65  66 /**  67  * 第22行代码处:  68  */  69 private T setInitialValue() {  70     //获取初始值  71     T value = initialValue();  72     //获取当前的线程  73     Thread t = Thread.currentThread();  74     //获取线程中的threadLocals  75     ThreadLocalMap map = getMap(t);  76     if (map != null)  77         //如果threadLocals存在,就往其中存放初始数据  78         map.set(this, value);  79     else  80         //如果ThreadLocalMap没有初始化,就完成相关初始化工作并放入初始数据  81         createMap(t, value);  82     //最后返回这个初始值就行了  83     return value;  84 }  85  86 /**  87  * 第71行代码处:  88  * 本方法默认返回null,可以由调用者覆写  89  */  90 protected T initialValue() {  91     return null;  92 }

9 remove方法

 1 /**   2  * ThreadLocal:   3  */   4 public void remove() {   5     //获取当前线程中的threadLocals   6     ThreadLocalMap m = getMap(Thread.currentThread());   7     if (m != null)   8         //如果不为null就进行删除(删除的是当前的ThreadLocal)   9         m.remove(this);  10 }  11  12 private void remove(ThreadLocal key) {  13     Entry[] tab = table;  14     int len = tab.length;  15     //获取哈希槽的位置  16     int i = key.threadLocalHashCode & (len - 1);  17     for (Entry e = tab[i];  18          e != null;  19          e = tab[i = nextIndex(i, len)]) {  20         //遍历table数组,如果ThreadLocal相等的话(如果不等说明发生了hash冲突,此时用线性探测的方式找寻下一个哈希槽)  21         if (e.get() == key) {  22             //就清除ThreadLocal  23             e.clear();  24             //同时尝试删除垃圾数据  25             expungeStaleEntry(i);  26             return;  27         }  28     }  29 }

10 InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的子类,通过前面可知,ThreadLocal只能在同一线程内进行变量的共享,而InheritableThreadLocal不仅可以在同一线程内进行变量的共享,而且可以在父子线程之间进行变量的共享。比如说在父线程a中创建了一个子线程b,那么在a线程中用InheritableThreadLocal包装的变量,在子线程b中也能获取的到。 但需要注意的是:InheritableThreadLocal和ThreadLocal一样,在同级线程中依然不能共享变量的值。并且InheritableThreadLocal只能父传子,不能子传父(同时也不是任何时候都能父传子,只有在一开始初始化的时候才会进行数据传递,后面会看到这点)。

 1 public class InheritableThreadLocal extends ThreadLocal {   2   3     protected T childValue(T parentValue) {   4         return parentValue;   5     }   6   7     ThreadLocalMap getMap(Thread t) {   8         return t.inheritableThreadLocals;   9     }  10  11     void createMap(Thread t, T firstValue) {  12         t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);  13     }  14 }

以上便是InheritableThreadLocal的全部源码,可见其只是覆写了ThreadLocal的三个方法而已。而在getMap和createMap方法中可以看到inheritableThreadLocals这个属性,那么这个属性到底是干什么的呢?其实它跟threadLocals属性一样,都是放在Thread类中的属性:

1 public class Thread implements Runnable {   2     //...   3   4     /* ThreadLocal values pertaining to this thread. This map is maintained   5      * by the ThreadLocal class. */   6     ThreadLocal.ThreadLocalMap threadLocals = null;   7   8     /*   9      * InheritableThreadLocal values pertaining to this thread. This map is  10      * maintained by the InheritableThreadLocal class.  11      */  12     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  13  14     //...  15 }

正是因为有了inheritableThreadLocals这个属性,就可以让子线程来访问父线程中的本地变量。

在创建线程的时候,会调用到Thread类的init方法:

 1 /**   2  * Thread:   3  */   4 private void init(ThreadGroup g, Runnable target, String name,   5                   long stackSize, AccessControlContext acc,   6                   boolean inheritThreadLocals) {   7     //...   8   9     Thread parent = currentThread();  10     //...  11     if (inheritThreadLocals && parent.inheritableThreadLocals != null)  12         /*  13         将父线程中inheritableThreadLocals的数据初始化到一个新的ThreadLocalMap中,  14         并赋值给子线程的inheritableThreadLocals  15          */  16         this.inheritableThreadLocals =  17                 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);  18     //...  19 }

这里省略了其他不相干逻辑,只需要来看一下inheritableThreadLocals的初始化过程,进一步跟踪第17行代码处:

 1 /**   2  * ThreadLocal:   3  */   4 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {   5     //parentMap是父线程中的数据   6     return new ThreadLocalMap(parentMap);   7 }   8   9 /**  10  * 将父线程中的inheritableThreadLocals复制到一个新的ThreadLocalMap中  11  * 个人认为这里直接将parentMap返回回去应该也是可以的,但这里重新构建一个  12  * ThreadLocalMap感觉是为了做一遍清理工作,将Entry为null的哈希槽清理掉  13  */  14 private ThreadLocalMap(ThreadLocalMap parentMap) {  15     Entry[] parentTable = parentMap.table;  16     int len = parentTable.length;  17     //设置子线程的threshold  18     setThreshold(len);  19     //初始化子线程的table(因为是在子线程创建的时候调用到这里的,所以不需要判断是否已经初始化,这里一定是未初始化的)  20     table = new Entry[len];  21  22     //遍历父线程中的table  23     for (int j = 0; j < len; j++) {  24         //获取其中的Entry  25         Entry e = parentTable[j];  26         if (e != null) {  27             //获取当前槽中的Entry中保存的ThreadLocal  28             @SuppressWarnings("unchecked")  29             ThreadLocal key = (ThreadLocal) e.get();  30             if (key != null) {  31                 //这里会调用InheritableThreadLocal覆写的childValue方法,也就是返回e.value本身  32                 Object value = key.childValue(e.value);  33                 //构建一个新的Entry  34                 Entry c = new Entry(key, value);  35                 //获取哈希槽的位置  36                 int h = key.threadLocalHashCode & (len - 1);  37                 //通过线性探测的方式来找到要插入的位置  38                 while (table[h] != null)  39                     h = nextIndex(h, len);  40                 //插入数据  41                 table[h] = c;  42                 //计数+1  43                 size++;  44             }  45         }  46     }  47 }  48  49 /**  50  * InheritableThreadLocal:  51  * 第32行代码处:  52  */  53 protected T childValue(T parentValue) {  54     return parentValue;  55 }

完整的流程如下:

  1. 首先父线程调用set或get方法时,会调用InheritableThreadLocal覆写的getMap方法返回inheritableThreadLocals,但因为其没有初始化,所以会调用覆写的createMap方法来创建ThreadLocalMap,并赋值给inheritableThreadLocals。这样父线程的inheritableThreadLocals属性就不为null了;接着父线程会调用set方法往父线程的inheritableThreadLocals属性中的table数组中进行赋值;

  2. 然后创建子线程的时候,会调用Thread类的init方法中的ThreadLocal.createInheritedMap方法。将父线程inheritableThreadLocals属性中的数据初始化到一个新的ThreadLocalMap中,并同时赋值给子线程的inheritableThreadLocals。这样子线程的inheritableThreadLocals属性中就有了父线程中的数据;

  3. 最后子线程在调用get方法的时候就能拿到父线程中的数据了。但是需要注意的是:子线程执行完毕后,父线程此时调用get方法拿到的还是之前父线程中的inheritableThreadLocals,并不是子线程会往其中更改过的ThreadLocalMap。也就是说子线程的数据不会传递给父线程,子线程只有在一开始初始化的时候才会同步父线程中的数据。

到此,相信大家对"ThreadLocal源码分析之入如何实现ThreadLocal"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

0