一、什么是ThreadLocal
ThreadLocal用于提供线程内部共享的变量,每个线程在访问ThreadLocal实例的时候都可以获得自己的、独立初始化的变量副本,这样线程间互不干扰,从而避免了线程安全问题。
比如我们知道SimpleDateFormat是线程不安全的,多个线程同时用一个SimpleDateFormat对象解析日期时间会报错,但是在一个线程中每解析一个数据就创建一个SimpleDateFormat对象显然也很浪费,这时候我们就可以通过ThreadLocal.withInitial()方法创建一个ThreadLocal实例,并设定变量初始化函数,那么每个线程在第一次调用get()方法时就会执行这个初始化创建自己的SimpleDateFormat对象并返回,之后在线程中每次调用get()都是返回这个自己的独立的对象,从而实现了线程内的变量共享,并且线程间互不干扰。
public class DateFormatTest {
public static void main(String[] args) {
for (int i=1;i<=3;i++) {
new Thread(() -> {
try {
System.out.println(CommonUtils.parseTime("12:23:11"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
class CommonUtils {
public static ThreadLocal<SimpleDateFormat> safeSdf =
ThreadLocal.withInitial(() -> new SimpleDateFormat("hh:mm:ss"));
public static long parseTime(String timeStr) throws ParseException {
return safeSdf.get().parse(timeStr).getTime();
}
}
除了withInitial()方法外,还可以直接调用ThreadLocal默认构造器创建实例,然后在需要设置线程共享变量时调用set()方法直接设置。
比如在请求Java后端的API服务时,http请求中经常会携带一些通用参数,比如token、uid、did这些,我们可以在请求预处理时将这些数据解析出来通过set()方法放到ThreadLocal中,然后在这个请求处理过程中此线程任意位置都可以直接访问到这些数据了,最后在请求结束后调用remove将数据移除就可以了。
public class TokenTL {
public static ThreadLocal<String> tokenTL= new ThreadLocal<>();
public static void setToken(String token) {tokenTL.set(token);}
public static String setToken() {return tokenTL.get();}
public static void removeToken() {tokenTL.remove();}
}
二、ThreadLocal的基本原理
线程Thread类中有一个ThreadLocalMap成员变量,一个线程所有通过ThreadLocal创建的独立变量实际上就是存放在这里面。
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
ThreadLocalMap本身是ThreadLocal的一个内部类,可以把它理解为ThreadLocal 类实现的定制化的 HashMap。内部也是通过一个Entry数组存储键值对,键是对ThreadLocal对象的弱引用,值就是存储的独立变量。
1. set()方法
调用threadLocal.set()方法时,首先获取当前线程中的ThreadLocalMap对象,如果为null则创建,然后以当前threadLocal弱引用为key,以要set的值为value,在这个map中插入或者更新值。
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2. get()
在调用threadLocal.get()方法时,就是在当前线程的ThreadLocalMap中以threadLocal实例引用为key查找对应的value,如果没找到,则看threadLocal实例有没有重写InitialValue()函数,就是前面说的withInitial()方法创建时设置的,如果有则初始化并设置value值,然后将其返回。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
3. remove()方法
调用threadLocal.set()方法时,清除当前线程的ThreadLocalMap中以当前ThreadLocal实例为key的Entry对象。
三、ThreadLocal内存泄漏问题
线程中ThreadLocalMap对象的引用链如下:
Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(threadLocal对象)和value
其中key 为 ThreadLocal 对象的弱引用,而 value 是强引用。所以,如果这个 ThreadLocal 对象没有被外部强引用的情况下,在垃圾回收的时候,key 会被自动清理掉,而 value 不会被清理掉。
虽然在调用 set()、get()方法时清理部分 key 为 null 的记录,但这显然是不完备的,最好还是在使用完 ThreadLocal方法后手动调用remove()方法进行清除。
不过可能通常在使用ThreadLocal时都是直接定义为类变量(static修饰),默认被类强引用
1. 为什么 ThreadLocalMap 的 key 设计为弱引用?
2. 为什么 ThreadLocalMap 的 value 设计为强引用?
【假设Entry 的 value 是弱引用】:假设 key 所引用的 ThreadLocal 对象还被其他的引用对象强引用着,那么这个 ThreadLocal 对象就不会被 GC 回收,但如果 value 是弱引用且不被其他引用对象引用着,那 GC 的时候就被回收掉了,那线程通过 ThreadLocal 来获取 value 的时候就会获得 null,显然这不是我们希望的结果。因为对我们来说,value 才是我们想要保存的数据,ThreadLcoal 只是用来关联 value 的,如果 value 都没了,还要 ThreadLocal 干嘛呢?所以 value 不能是弱引用。
参考:https://zhuanlan.zhihu.com/p/513517989