本篇内容主要讲解“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”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。