ThreadLocal到底是什么?
首先看看如何使用:

从上面结果看5个线程通过ThreadLocal拿到的变量互不影响。可以达到线程安全。
那么他是如何做到的呢?底层结构如何?
翻看ThreadLocal源码,并没有看到有存储数据的成员属性,那么数据存在哪里呢?
通过查看get方法源码,这个方法不用任何参数, 发现他是先获取当前线程对象,然后从当前线程对象获取一个ThreadLocalMap对象,然后从这个map对象把this(当前ThreadLocal对象)获取数据。
public T get() {//获取当前线程对象Thread t = Thread.currentThread();//从线程对象获取ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//当前ThreadLocal对象,从map中查找值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//ThreadLocalMap还未初始化,创建一下并赋予初始值return setInitialValue();}
初始化ThreadLocalMap
private T setInitialValue() {//获取初始化值T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else//创建ThreadLocalMap并初始化值createMap(t, value);return value;}
初始化线程t的ThreadLocalMap,并赋值
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
接下来看下ThreadLocalMap在Thread中是怎么存放的。
public class Thread implements Runnable {//线程成员变量,也就是每个线程都有个ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals = null;///线程成员变量,这个后面再分析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,设置数据,key是ThreadLocal对象。map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}//第一次,将线程ThreadLocalMap初始化好。void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
下面注意看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)]) {//keyThreadLocal<?> k = e.get();//ThreadLocal找到了 替换旧值if (k == key) {e.value = value;return;}//key已经被回收了if (k == null) {//陈旧数据替换 替换成本次新set的key,valuereplaceStaleEntry(key, value, i);return;}}//构建新节点,存到下标i位置tab[i] = new Entry(key, value);int sz = ++size;//是否要扩容了if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}/*** Increment i modulo len.*/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))//找到最小的一个被回收的下标 默认是staleSlotif (e.get() == null)slotToExpunge = i;for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//找到了相同的keyif (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没有找到无效的keyif (slotToExpunge == staleSlot)//slotToExpunge 设置成无效slotToExpunge = i;// 回收slotToExpungecleanSomeSlots(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为最大需要回收的keyif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (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);//无符号右移动一位,可以简单理解为除以2return 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;}//获取线程的inheritableThreadLocalsThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}//设置线程的inheritableThreadLocalsvoid createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}}//创建线程的地方,会初始化inheritableThreadLocals变量if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
在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在框架中使用的比较多,工作中也有可能用的到,实现线程间数据独占使用,保证线程安全,了解一些原理对工作中也有帮助。










