ThreadLocal 是线程私有领地,其他线程无法访问。
本文首先给出一个简单的使用示例,接下来主要解析ThreadLocal的原理,继而作图以生动地表示ThreadLocal的底层存储关系,探索可能出现的内存泄漏,最后给出一个ThreadLocal的正确使用姿势。
一个初级小栗子
你知道吗?据说这段代码可能会造成内存泄漏,,,什么,不知道?
无妨,写这篇文章之前偶也不知道,一起来学习下吧。
public static void main(String[] args) {
//tl 是强引用,指向ThreadLocal对象
final ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("ThreadLocal Test");
new Thread(() -> {
System.out.println("t1 print tl: " + tl.get());
}, "t1").start();
System.out.println("main thread print tl: " + tl.get());
}
解析ThreadLocal
明确一个问题
我们知道ThreadLocal用于存储线程私有变量,那么变量存在哪里呢?
答:
见Thread类中的一行代码。私有变量就存储在线程自己的threadLocals中,它是一个ThreadLocalMap。其中ThreadLocal对象作为map的key,存储的变量作为map的值。
ThreadLocal.ThreadLocalMap threadLocals = null;
主要源码分析
public void set(T value) {
//拿到当前线程 t
Thread t = Thread.currentThread();
/**
* 取得当前线程 t 的threadLocals ,
* 上文提到过,Thread类中有一个ThreadLocal.ThreadLocalMap
* 类型的threadLocals
*/
ThreadLocalMap map = getMap(t);
if (map != null)
//把变量保存到map中,此处的this代表当前ThreadLocal对象---tl
//value代表存储的变量---"ThreadLocal Test"
map.set(this, value);
else
createMap(t, value);
}
public T get() {
//获取当前线程 t
Thread t = Thread.currentThread();
//拿到 t 的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//以ThreadLocal对象为key,查询对应的私有变量
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
以上,set、get方法源码解析完毕,只有寥寥数行代码,相当简单。若还是似懂非懂,无妨,看我作图:
该图描述了ThreadLocal变量的存储关系。分析一下这个图:
- Thread中持有对ThreadLocal对象的引用 tl,这是一个强引用(R1);还持有对ThreadLocalMap 的引用 threadLocals,这也是一个强引用(R3)。
- ThreadLocalMap中的key引用了ThreadLocal对象,这是一个弱引用(R2)。
- ThreadLocalMap中的每一个Entry都继承自WeakRefence。
内存泄漏分析
为什么R2是弱引用?
答:弱引用特性:只有弱引用指向的对象,每次gc都会被回收。这样子,当R1断开后,因为唯一指向ThreadLocal对象的引用R2是弱引用,在下次gc时ThreadLocal对象就会被回收,从而不会造成内存泄漏。tips:因此每次使用完ThreadLocal,要记得显示指定 tl = null;
还存在内存泄漏吗?
答:存在。
若ThreadLocal对象被回收,那么key就会变为null,那么我们再也无法通过原来的key值获得其对应的value。
因为R3是强引用,所以ThreadLocalMap会一直存在,进而Value被ThreadLocalMap通过R4(强引用)所引用,也会一直存在。
Value一直存在于内存,而再也不会被访问到,这也是内存泄漏。tips:每次用完ThreadLocal,显示地删除这个变量。tl.remove();
完善的小栗子
经过以上学习,下面给出一个笔者认为完善的示例,防止内存泄漏。
public static void main(String[] args) {
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("ThreadLocal Test");
//省略中间的应用逻辑
...
//防止value内存泄漏
tl.remove();
//防止key内存泄漏
tl = null;
}
应用场景
- 框架中的应用。Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。
- 实际工作中的应用。我们知道SimpleDataFormat不是线程安全的。所以如果声明为全局变量会有并发安全隐患。一般会在使用这个类的方法里创建SimpleDataFormat对象,这样不会有并发隐患。但是方法内部的对象在方法执行完其生命周期就结束了,如果一个请求在调用链路的n个方法中都调用了SimpleDataFormat,是不是要创建n个对象?此时我们可以将其声明为ThreadLocal,一个线程在其生命周期内使用同一个SimpleDataFormat,不同对象之间使用各自的SimpleDataFormat对象,形成线程间的数据隔离,不会有并发安全隐患。然而这种场景,适用于一个线程有多个方法调用都用到了SimpleDataFormat,可以减少创建对象的次数。如果生命周期内只有一个方法使用了这个对象,在方法里创建就好了。
- 用于传递参数。项目中,一个线程横跨若干个方法调用,需要传递的对象,也就是上下文,它是一种状态,经常就是用户身份、任务信息等,就会存在过渡传参的问题。在线程最开始设置ThreadLocal对象,在线程生命周期的多个方法执行过程中,通过tl.get()就可以取出参数。这种方法可以避免通过方法入参传递变量。
补充
Thread中还存在一个属性inheritableThreadLocals,可以继承父线程中的inheritableThreadLocals 的值。
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
创建线程对象的时候,有如下一行代码。若inheritThreadLocals 是true,且父线程的inheritableThreadLocals 不是null,则当前线程的inheritableThreadLocals 使用父线程中inheritableThreadLocals 的值。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);