温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

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

发布时间:2021-10-23 16:51:22 来源:亿速云 阅读:137 作者:iii 栏目:编程语言

本篇内容主要讲解“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<ThreadLocal<?>> {  
 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的值的引用链,如下图所示:

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

其上的虚线为弱引用,实线为强引用。当使用完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<DateFormat> 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<T> {  
 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。这里的 ThreadLocal源码分析之入如何实现ThreadLocal , 其 中的φ也就是所谓的黄金分割率,也就是近似于0.618的那个数。 而黄金比例又与斐波那契数( ThreadLocal源码分析之入如何实现ThreadLocal )之间有密切关系,使用这个数时会使得 散列的结果很均匀:

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

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<T> extends ThreadLocal<T> {  
 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<Object> key = (ThreadLocal<Object>) 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”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI