ThreadLocal主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程是相对隔离的,在多线程的情况下,防止自己的数据被别的线程修改。
ThreadLocal在面试中也有很多面试官喜欢问,很多人认为数据是存在ThreadLocal中的,key是当前线程。
首先这样是错误的,实际上java中线程共享机制,最重要的是Thread中的ThreadLocalMap,ThreadLocal并不重要,它只是一个”钩子“,数据实际上被存在ThreadLocalMap中,下面慢慢道来:
ThreadLocal的使用public class Test01 {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set("lisi");
String s = threadLocal.get();
System.out.println(s);
new Thread(new Runnable() {
@Override
public void run() {
String s1 = threadLocal.get();
System.out.println(s1);
}
}).start();
}
}
结果:
lisi
null
ThreadLocal.set(T value)的key是Thread吗?
首先,先告诉大家,ThreadLocal并不保存数据,ThreadLocalMap的key也不是Thread。
那为什么会有人会认为key是当前线程呢?看看set()的源码
createMap(t, value),传入了一个Thread和value,很多人就会认为ThreadLocal内部有一个Map,key就是Thread了,但事实是这样的吗?
并不是,看逻辑,先不管map是哪里来的,if(map !=null) map.set(this,value);也就是把this作为了map的key,那么这个this是谁呢,很显然是ThrealLocal。
再来说说map是怎么来的?
答案就在createMap(t,value)中,t是当前线程:
原来是通过create()方法new了一个ThreadLocalMap,并做了初始化,把当前ThreadLocal对象作为了key,firstValue作为了value。
到这里我们至少知道了两点:
- 有一个map,来历还不知道,数据存在这里,key是当前ThreadLocal对象,而不是当前线程
- Map类型是ThreadLocalMap,被赋值给Thread中的一个字段threadLocals
接着来:
如何理解ThreadLocal是一个钩子?
从上图可以看出,Thread t1 实例化后,内部会有一个ThreadLocalMap,刚开始为null,当执行到
ThreadLocal tl1 = new ThreadLocal();
t1.set("你好");
ThreadLocal会作为一个钩子,尝试从Thread T1中勾出ThreadLocalMap,如果发现这个成员变量尚未赋值,则new ThreadLocalMap()并把map设置进去。特别注意,由于set()是ThreadLocal的方法,所以map.set(this, value)中的this显然是ThreadLocal tl1。
此时此刻,内存中有三个对象,Thread t1、ThreadLocal tl1、ThreadLocalMap map,其中ThreadLocalMap在Thread内部,是ThreadLocal塞进去的。
ThreadLocalMap底层是什么样子的呢?既然有个map,那他的数据结构应该和HashMap很像,但是从源码上看,它并未实现map接口,而且它的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
大概就是这样了:
用数组是因为,我们开发过程中可以一个线程可以有多个ThreadLocal来存放不同类型的对象,他们都将存放在当前线程的ThreadLocalMap中,需要用数组接收。
至于Hash冲突,我们先看一下源码:
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从源码可以看到,ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
然后会判断,如果当前位置是空的,就初始化一个Entry对象放在位置i上;
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
如果不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
以下是get的源码,是不是就感觉很好懂了:
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
对象存放在哪里?
在Java中,栈内存归属于每一个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程可见,即栈内存是线程私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
那么是不是说ThreadLocal的实例以及数据存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
结果:
这又是怎么传递的呢?
Thread源码中,我们看看Thread.init初始化创建的时候做了什么:
public class Thread implements Runnable {
……
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
……
}
如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。
多个Thread与同一个ThreadLocal为什么访问同一个threadLocal.get(),Thread1存入的值不会被Thread2取出来?
上代码:
ThreadLocal zixia = new ThreadLocal();
// 至尊宝
new Thread(()->{
// 遇到紫霞,紫霞往他心里塞了眼泪
zixia.set("泪水");
}).start();
// 孙悟空
new Thread(()->{
// 遇到紫霞,紫霞往他心里塞了紫青宝剑
zixia.set("紫青宝剑");
}).start();
一个线程中有一个ThreadLocalMap,至尊宝这个线程中ThreadLocalMap保存了已zixia为key,”泪水“为value的entry;孙悟空也在自己的线程中保存了一个entry,两个线程里面的东西都不一样,又怎么会串起来呢?
同一个Thread与多个ThreadLocalThreadLocal zixia = new ThreadLocal();
zixia.set("泪水");
ThreadLocal baijingjing = new ThreadLocal();
baijingjing.set("回忆");
一个Thread中有一个ThreadLocalMap,第一次遇到的ThreadLocal会帮他创建一个map塞进去,之后无论遇到多少个ThreadLocal,都是直接用那个map,并把自己作为key,存到map中。
多个Thread与多个ThreadLocalThreadLocalMap本质是每个Thread内部各存一份,互不干扰。Thread在遇到不同的ThreadLocal,可以把ThreadLocal自身作为key存入map或从map中取出value。
ThreadLocalMap和WeakReference(弱引用)在java中有4中类型的引用:强、软、弱、虚。
- 强引用不受GC的影响,除非引用全部切断,Student student = new Student();这就是强引用,当student=null,student对象会在下次GC时被回收。
- 软引用对象会在内存不足触发GC时被回收(适用于高速缓存)
- 弱引用是每次GC时都回收
- 虚引用(堆外内存,比如zerocopy)
对于Map,每一个键值对被称为Entry,相信大家都知道。
为什么ThreadLocalMap的Entry要继承弱引用呢?
虽然Entry继承了WeakReference,但并不是说Entry本身是弱引用,而是Entry的key是弱引用:
于是问题由为什么ThreadLocalMap的Entry要继承弱引用转为ThreadLocalMap为什么要把key包装成弱引用。
如果ThreadLocalMap的key是强引用,那么只要线程存在,ThreadLocalMap就存在,而ThreadLocalMap是entry数组,对应的entry数组就存在,entry的key是ThreadLocal,即使我们在代码中显示的赋值threadlocal=null,告诉CG要回收该对象,由于上面的强引用存在,不会被回收的。
原本引入Weak Reference是为了解决多个强引用导致ThreadLocal对象无法回收的问题,但一个解决策略的引入往往伴随着新bug的产生。试想一下,当外部强引用都切断后下一次GC回收了ThreadLocal对象,此时Entry的key会变成什么?
当tl1变成null,ThreadLocalMap的Entrys变成下面这样:
● null : value1(Entry1)
● tl2 : value2(Entry2)
● tl3 : value3(Entry3)
这就导致了一个问题,ThreadLocalMap中的key是弱引用,在没有外部强引用时,下次GC会被回收,如果创建的ThreadLocal线程一直持续运行,那么这个entry数组中的value就有可能一直不会被回收,发生内存泄漏。
比如线程池中的线程,线程都是复用的,那么之前的线程实例处理完之后,处于复用的原因线程依然存活(线程复用就是将线程阻塞,线程中一直持有entry数组,导致不会被GC回收),最终导致内存泄漏。
怎么解决?在代码的最后一行remove就好了:
ThreadLocallocalName = new ThreadLocal(); try { localName.set("张三"); …… } finally { localName.remove(); }
实际上,ThreadLocalMap也发现了这个问题,它会在每次get/set时判断key,如果key为null,则把value也归置为null:
当这种策略有风险,因为它的前提是下一次使用时会把上一次的key为null的清除,如果在也不用,是不是仍然没有清除。
所以最保险的方法是,每次使用完毕都及时清除。
看一下remove方法:
《阿里巴巴开发手册》也是这样建议:
一般来说,用完立即移除是最好的,但实际编程时有些公司喜欢在拦截器中取出用户信息放入线程,对此个人建议可以在拦截器的preHandle()中set,在afterCompletion()中remove()。
封装ThreadLocal 为什么要封装ThreadLocal原因有两点:
- 对于Thread,如果希望在Interceptor中存入UserInfo并在Service层通过ThreadLocal把UserInfo钩出来,必须保证Interceptor和Service此时用的是同一个ThreadLocal。
我们可以创建一个ThreadLocalUtil中new一个ThreadLocal对象作为成员变量,就可以在service中取出来了:
public class ThreadLocalUtil{
private ThreadLocalUtil(){
};
private static final ThreadlLocal
2. 原生的ThreadLocal无法满足复杂的业务场景
比如现在我封装了一个最简单的ThreadLocal(装饰者模式,为的是解决第一个问题):
package com.example.czy.util;
public class MyThreadLocal {
public MyThreadLocal(){
};
private static final ThreadLocal
MyThreadLocal确实解决了第一个问题,复用了ThreadLocal,保证了Interceptor和Service用到的ThreadLocal是同一个对象。
但是,有两个缺陷:
● 无法存取多个不同的值
● 语意不明
比如,Service层希望往ThreadLocal里再添加一个Score对象,好让DAO层能获取到。你要怎么做?
public class ThreadLocalUtil {
public ThreadLocalUtil(){
}
private static final ThreadLocal
spring对ThreadLocal的封装
比如编写AOP日志时,经常会用到的RequestContextHolder,其实内部也维护了ThreadLocal。
那么Spring是如何做到remove的呢?使用过滤器(我们使用了拦截器)。



