/线程成员变量,这个后面再分析 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;}
从线程类源码看出,每个线程对象都会有一个ThreadLocalMap属性。
可以得出结论:ThreadLocal其实不存储数据,他是一个工具类,间接操作Thread对象中的ThreadLocalMap变量的。
既然数据都存在ThreadLocalMap,我们分析ThreadLocalMap的结构和底层实现。
首先理一下ThreadLocalMap和线程,还有ThreadLocal三者的关系
通过这个图可以看出Thread类中持有一个ThreadLocalMap引用,其实就是一个Entry类型的数组。Entry的key是ThreadLocal类型的,value 是Object 类型。也就是一个ThreadLocalMap可以持有多个ThreadLocal。
捋一捋关系:
1、Thread持有ThreadLocalMap的引用,他们是1对1关系。
2、Entry是ThreadLocalMap的内部类,并且ThreadLocalMap持有Entry类型的数组。也就是一个ThreadLocalMap对应多个Entry。
3、ThreadLocal和ThreadLocalMap的关系是最难描述的,因为
ThreadLocalMap是ThreadLocal的子类,而ThreadLocalMap中存储的key类型是ThreadLocal,并且ThreadLocal是弱引用类型的。
看下他们的代码关系:
public class ThreadLocal<T> { //内部类 static class ThreadLocalMap { /** * 存储数据的条目,key是WeakReference弱引用类型的ThreadLocal, * key直接用WeakReference管理。 * 如果get方法(key==null)锁门条目不存在了,会主动清除, * 避免内存泄漏的(分配的内存,没用了,但是没被回收) */static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
为什么ThreadLocalMap设计成内部类?
主要是说明ThreadLocalMap 是一个线程本地的值,它所有的方法都是private 的,也就意味着除了ThreadLocal 这个类,其他类是不能操作ThreadLocalMap 中的任何方法的,这样就可以对其他类是透明的。同时这个类的权限是包级别的,也就意味着只有同一个包下面的类才能引用ThreadLocalMap 这个类,这也是Thread 为什么可以引用ThreadLocalMap 的原因,因为他们在同一个包下面。
虽然Thread 可以引用ThreadLocalMap,但是不能调用任何ThreadLocalMap 中的方法。这也就是我们平时都是通过ThreadLocal 来获取值和设置值。
这样设计的好处是什么?
ThreadLdocalMap 对使用者来说是透明的,可以当作空气,我们一直使用的都是ThreadLocal,这样的设计在使用的时候就显得简单,然后封装性又特别好。
set方法源码分析:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null)
下面注意看ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {//我们不像get()那样使用快速路径,因为使用set()创建新条目//与替换现有条目至少一样普遍,在这种情况下,快速路径经常会失败。 Entry[] tab = table; int len = tab.length; //计算下标 //哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正 int i = key.threadLocalHashCode & (len-1); //这里不断找下一个下标,直到找到数组下标位置为null的下标 //这里处理hash冲突,使用的是线性探测方法。 for (Entry e = tab[i]; e != null; //线性探测方法 解决hash冲突 e = tab[i = nextIndex(i, len)]) { //key ThreadLocal<?> k = e.get(); //ThreadLocal找到了 替换旧值 if (k == key) { e.value = value; return; } //key已经被回收了 if (k == null) { //陈旧数据替换 替换成本次新set的key,value replaceStaleEntry(key, value, i); return; } } //构建新节点,存到下标i位置 tab[i] = new Entry(key, value); int sz = ++size; //是否要扩容了 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private static int nextIndex(int i, int len) { //每次下标加1,线性查找 return ((i + 1 < len) ? i + 1 : 0); }
看完上面的方法,就有疑问了,为什么ThreadLocalMap采用开放地址法来解决哈希冲突?
jdk 中大多数的Hash类都是采用了链地址法来解决hash冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式:
1、链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在这个链中进行。
2、开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法和开放地址法的优缺点
开放地址法:
1、容易产生堆积问题,不适于大规模的数据存储。
2、散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
3、删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
链地址法:
1、处理冲突简单,且无堆积现象,平均查找长度短。
2、链表中的结点是动态申请的,适合构造表不能确定长度的情况。
3、删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
指针需要额外的空间,故当结点规模较小时,开放地址法较为节省空间。
ThreadLocalMap采用开放地址法原因
1、ThreadLocal类中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table。
2、ThreadLocal 往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。
自动清理源码:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) //找到最小的一个被回收的下标 默认是staleSlot if (e.get() == null) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //找到了相同的key if (k == key) { e.value = value; //交换位置,这个时候staleSlot位置上的变成新的有用的数据 i位置无用 //为什么要交换 不交换 的时候大坐标位置上存key 下次set会直接存入小下标位置 导致两个相同的key 出现数据错乱问题 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists 没有找到无效的key if (slotToExpunge == staleSlot) //slotToExpunge 设置成无效 slotToExpunge = i; // 回收slotToExpunge cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. // 更新slotToExpunge为最大需要回收的key if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null; tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { //这里设置为null ,方便让GC 回收 e.value = null; tab[i] = null; size--; } else { //这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作 //处理,可以简单理解就是让后面的元素往前面移动 //为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null //的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明 int h = k.threadLocalHashCode & (len - 1); //他们不相等,说明是经过hash 是有冲突的 if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
//这个方法是从i 开始往后遍历(i++),寻找过期对象进行清除操作 private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; // 用do while 语法,保证 do 里面的代码至少被执行一次 do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { //如果遇到过期对象的时候,重新赋值n=len 也就是当前数组的长度 n = len; removed = true; //在一次调用expungeStaleEntry 来进行垃圾回收(只是帮助垃圾回收) i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0);//无符号右移动一位,可以简单理解为除以2 return removed; }
通过查看上面的源码,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,我们还可以发现get和set方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的,但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出,退一步说,就算我们没有调用get和set和remove方法,线程结束的时候,也就没有强引用再指向ThreadLocal中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一种危险是,如果线程是线程池的,在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap和里面的元素是不会回收掉的。
关于ThreadLocal的思考
ThreadLocal找到空key时候尝试清理一遍无效的entry,此时向前遍历是为了找到一个最小需要清理的entry下标。向后遍历是为了找到第一个相同key的下标,这里是为了解决key相同的时候,判断下标出错,有用的数据往前移动。
ThreadLocalMap的key设计为弱引用,可以起到标识key失效了,需要被回收,使用线性探测进行回收清理失效的数据。
ThreadLocal两种清除方式分开讨论
他们的应用场景不一样 remove方法,主动清除数据的机制。
而 set/get方法里的清理逻辑 是针对 ThreadLocal WeakReference.get=null 这个对象被回收了,value还存在的情况。
当一个ThreadLocal失去强引用,生命周期只能存活到下次gc前,此时ThreadLocalMap中就会出现key为null的Entry,当前线程无法结束,这些key为null的Entry的value就会一直存在一条强引用链,造成内存泄露。
解决方案:
建议将ThreadLocal变量定义成private static的,在调用ThreadLocal的get()、set()方法完成后,再调用remove()方法,手动删除不再需要的ThreadLocal。
InheritableThreadLocal 理解
InheritableThreadLocal是ThreadLocal的子类,作用是用来共享父类的ThreadLocal数据。使用方法和ThreadLocal一样,通过模版方法设计模式,重写了getMap,createMap。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; }
在Spring框架的web模块就用到了ThreadLocal和InheritableThreadLocal。用来对每个线程的请求Request属性进行存储。
public abstract class RequestContextHolder { private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader()); private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");}
ThreadLocal在框架中使用的比较多,工作中也有可能用的到,实现线程间数据独占使用,保证线程安全,了解一些原理对工作中也有帮助。