ThreadLocal是线程本地变量,可以让每一个线程都拥有自己专属的本地变量,实现了线程的数据隔离。可以通过get()、set()来获取或更改值。
使用场景
- 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
- 在hiberate中管理session。
- 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
- 获取当前登录用户上下文。
- 临时保存权限数据。
- 使用MDC保存日志信息。
你工作中是否使用到ThreadLocal?
用的还真不多。项目中的工具类中有使用到,一个是DateUtils对时间进行格式化,另一个是NumberFormatUtil对数字的小数位进行保留。因为SimpleDateFormat和DecimalFormat都不是线程安全的。
NumberFormatUtil
public class NumberFormatUtil {
private static final ThreadLocal<DecimalFormat> decimalFormatThreadLocal = new ThreadLocal<DecimalFormat>() {
@Override
protected DecimalFormat initialValue() {
return new DecimalFormat("0.00");
}
};
/**
* 保留两位小数
* @return
*/
public static final String doubleToString(double value){
return decimalFormatThreadLocal.get().format(value);
}
}
DateUtils
public class DateUtils {
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
public static final Date convert(String source) throws ParseException {
return df.get().parse(source);
}
}
ThreadLoacl原理
ThreadLocal是线程的本地变量,实际上存储数据的并不是ThreadLocal,而是TreadLocalMap,每个线程(Thread类)中都有一个ThreadLocalMap(类似HashMap),key为ThreadLocal,value就是你set的值。(因为你一个线程可能会拥有多个ThreadLocal,所以要用一个Map来装着)
public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
ThreadLocal 类的 set() 方法
public class ThreadLocal<T> {
...
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据threadLocal对象从map中获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取保存的数据
T result = (T)e.value;
return result;
}
}
//初始化数据
return setInitialValue();
}
private T setInitialValue() {
//获取要初始化的数据
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将初始值设置到map中,key是this,即threadLocal对象,value是初始值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
return value;
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将值设置到map中,key是this,即threadLocal对象,value是传入的value值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
}
static class ThreadLocalMap {
...
}
...
}
ThreadLocalMap
static class ThreadLocalMap {
/**
* 键值对实体的存储结构
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// 当前线程关联的 value,这个 value 并没有用弱引用追踪
Object value;
/**
* 构造键值对
*
* @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
* @param v v 作 value
*/
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量,必须为 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
private Entry[] table;
// ThreadLocalMap 元素数量
private int size = 0;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
}
ThreadLocalMap中包含了一个静态的内部类Entry,它继承了WeakReference。说明Entry是弱引用。
而Entry是否ThreadLocal作为key,value是你传进来的对象。
get()
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据不为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
private T setInitialValue() {
//initialValue是开放的一个模板方法,子类可以实现初始化生成一个value的方法,类似上面的阿里使用方式
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
getEntry()
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);
}
描述了threadLoca的的hash冲突策略采用的是开放定址法,冲突+1。
ThreadLocal内存泄漏问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用(),而value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话, value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种 情况,在调用 set() 、 get() 、 remove()方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用remove() ⽅法
弱引用
- 如果⼀个对象只具有弱引用,那就类似于可有可无的生活用品。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引用的对象。 弱引用可以和⼀个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收, Java虚拟机就会把这个弱引用加⼊到与之关联的引用队列中。
解决办法:
- 将ThreadLocal设置为空之前,执行remove()方法,会将key为空的键值对清空。
- 尽量将ThreadLocal设置成static。
- 非必要尽量不要在ThreadLocal中放大对象。
ThreadLocal、ThreadLocalMap、Thread三者之间的关系
![image-20230921210108735](https://img-blog.csdnimg.cn/img_convert/464593c7e7f96fb045ecc849994cfff2.png)
Thread类中已经包含了ThreadLocalMap,因此Thread在最外层。而ThreadLocalMap的key是ThreadLocal,因此ThreadLocalMap在第二层。
她们之间的引用关系:
![image-20230921210835968](https://img-blog.csdnimg.cn/img_convert/a80025d7d4aceee92d070092ebd78d52.png)
上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。
为什么用ThreadLocal做key?(为什么变量不是直接放到ThreadLocal上)
- 如果你的项目中,只有一个ThreadLocal的话,直接通过ThreadLocal保存/用Thread作为key当然是可以的。
- 但是如果我们项目保存多个ThreadLocal怎么办呢?
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}
因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法。
为什么Entry的key为什么设计成弱引用?
- 目的:尽最大可能避免内存泄漏!
引用关系:
![image-20230921210835968](https://img-blog.csdnimg.cn/img_convert/a80025d7d4aceee92d070092ebd78d52.png)
- 当ThreadLocal变量使用完后,对ThreadLocal对象的引用会被置为null。
- 而key对ThreadLocal对象是弱引用;即:随时可被回收。因此,key也会被置为null。
![image-20230921214246153](https://img-blog.csdnimg.cn/img_convert/ed69166cd8511204dae0fcaa8d45626a.png)
关键点来了,ThreadLocal为了尽最大可能避免内存泄漏,是做了一些特殊处理的!
- 此时,ThreadLocal变量为null。如果现在存在另外一个ThreadLocal变量b,并且调用了get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。(ThreadLocal可以通过key.get()==null来判断Key是否已经被回收,如果Key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。)因此,此时的Entry(ThreadLocalMap)就会被回收掉。
需要特别注意的地方是:
- key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?(重点)
- 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。
那么,为什么不是value设计成弱引用呢?
- 你value设置成弱引用了,那不就随时能被回收了吗?
- key设置成弱引用不被回收,那是因为它弱引用了一个被强引用的对象。
如何实现父子线程共享数据?(线程池如何共享数据?)
- 难道你们忘了吗?Thread类中还有另外一个变量:InheritableThreadLocal。
- 使用方法和ThreadLocal类似,这里就不介绍拉!
ThreadLocal用完后一定要清理吗?你觉得为什么一定要remove?(阿里一面)
- 答案是不一定要清理的。例如:SimpleDateFormat。
- 因为我们往ThreadLocal中存放的是new SimpleDateFormat(),并不是存放具体的值。那么,不清理然后tomcat中的线程得到了这个复用,也不会产生问题。
网上很多人建议使用static修饰ThreadLocal,为什么?
- ThreadLocal实现线程的数据隔离,并不在于自己本身,而是ThreadLocalMap。所以ThreadLocal可以只初始化一次。
部分引用:苏三说技术博主的文章