本篇内容主要讲解“ThreadLocal源码分析之入如何实现ThreadLocal”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“ThreadLocal源码分析之入如何实现ThreadLocal”吧!
ThreadLocal是线程本地变量(缓存),其往往用来实现在同一线程内部的变量进行交互的情景,不存在线程之间的交互。其对每一个线程内部都维护了一个数据,在a线程set的值,也只能在a线程里进行get。
具体的使用场景:比如可以用ThreadLocal来封装数据库连接;也可以在复杂逻辑下,用ThreadLocal来作为方法之间的数据传递:如果一开始设置了一个数据,但因为调用逻辑复杂,跨越了很多的类和方法后,需要再次获取到这个数据,那么这个时候就可以用ThreadLocal来对这个数据进行缓存,访问就很方便了。但需要注意的是,这一切操作都必须保证是在同一个线程中进行的,也就是在同一个线程中进行set,也在同一个线程中进行get。在Spring框架中除了使用HashMap和ConcurrentHashMap来做各种缓存之外,也会用到ThreadLocal来缓存数据。
初学ThreadLocal可能会认为它跟synchronized和ReentrantLock是同样的同步机制(我之前也写过对ReentrantLock进行源码分析的文章 AQS源码深入分析之独占模式-ReentrantLock锁特性详解),但实际上他们完全是两种东西。实现的方式不同,解决的场景也不同。synchronized和ReentrantLock使用的是时间换空间的思路,是用来在多线程场景中访问共享变量用的。获取不到资源的线程会进行排队,等待去获取;而ThreadLocal使用空间换时间的做法,是用来做数据隔离和单个线程内的数据共享用的。set方法只会在当前线程中缓存数据,而get方法也只会在当前线程中获取到这个数据,在别的线程中调用get方法是获取不到的(在别的线程中调用get方法只会获取到ThreadLocal在那个线程中缓存的值)。
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的值的引用链,如下图所示:
其上的虚线为弱引用,实线为强引用。当使用完Thread对象后需要被回收时,在下一次gc的时候,因为Entry连着的ThreadLocal引用是弱引用,所以Thread对象能够顺利被回收掉。而如果是强引用,并且ThreadLocal是用static修饰的话,可能就不会被回收,从而产生内存泄漏的问题。
虽然上面使用了弱引用,以此来保证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。
这是《阿里巴巴编码规范》中的一条。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 }
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。这里的 , 其 中的φ也就是所谓的黄金分割率,也就是近似于0.618的那个数。 而黄金比例又与斐波那契数(
)之间有密切关系,使用这个数时会使得 散列的结果很均匀:
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 }
在上面第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 }
在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 }
之前已经说过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 }
在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 }
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 }
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 }
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 }
完整的流程如下:
首先父线程调用set或get方法时,会调用InheritableThreadLocal覆写的getMap方法返回inheritableThreadLocals,但因为其没有初始化,所以会调用覆写的createMap方法来创建ThreadLocalMap,并赋值给inheritableThreadLocals。这样父线程的inheritableThreadLocals属性就不为null了;接着父线程会调用set方法往父线程的inheritableThreadLocals属性中的table数组中进行赋值;
然后创建子线程的时候,会调用Thread类的init方法中的ThreadLocal.createInheritedMap方法。将父线程inheritableThreadLocals属性中的数据初始化到一个新的ThreadLocalMap中,并同时赋值给子线程的inheritableThreadLocals。这样子线程的inheritableThreadLocals属性中就有了父线程中的数据;
最后子线程在调用get方法的时候就能拿到父线程中的数据了。但是需要注意的是:子线程执行完毕后,父线程此时调用get方法拿到的还是之前父线程中的inheritableThreadLocals,并不是子线程会往其中更改过的ThreadLocalMap。也就是说子线程的数据不会传递给父线程,子线程只有在一开始初始化的时候才会同步父线程中的数据。
到此,相信大家对“ThreadLocal源码分析之入如何实现ThreadLocal”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://my.oschina.net/qiketime/blog/4753188