ThreadLocal:线程本地变量
ThreadLocal:线程本地变量,可以起到线程隔离作用,即每个线程访问自己的变量,不再是共享一个变量。
ThreadLocal利用泛型来封装「任意的自定义类」,我们定义ThreadLocal封装的任意的自定义类为「资源」,ThreadLocal就是在「资源」的基础上做了一层封装
应用场景
最常见的「资源」是数据库连接、simpleDateFormat等;
要么是线程独享的对象,要么是便于在线程生命周期的任意时间点方便的拿到。
在业务代码中,通常会通过定义一个全局的类对ThreadLocal进行封装,从而使得web服务器的请求线程可以轻松的拿到关键的业务数据,比如userId
快速使用
public class ThreadLocaDemo {
private static ThreadLocal localVar = new ThreadLocal();
new Thread(()->{
localVar.set("local_A");
System.out.println(localVar.get());
}).start();
}
三个API:增删改查
- set(T):增/改
- remove:删
- get:查
ThreadLocal实现原理
既然ThreadLocal是对「资源」的封装,那我们第一个要解决的问题就是:ThreadLocal如何存储资源? 你会发现ThreadLocal内部没有任何字段保存了资源,那么资源去哪里了?
- JDK8之前,ThreadLocal维护ThreadLocalMap,以Thread为key存对应的数据
- 现在Thread维护ThreadLocalMap,以ThreadLocal为key存对应的数据
下面基于JDK8源码分析
线程存储ThreadLocalMap
ThreadLocalMap是一张哈希表。key是ThreadLocal,value是ThreadLocal封装的「资源」。
ThreadLocalMap所有方法都是private的,没有向外暴露任何方法,因此如果仅仅想知道ThreadLocal的工作原理,我们完全不需要关注它的实现原理。
我们暂时可以把ThreadLocalMap理解成类HashMap,提供了类似哈希表数据结构的get/set/remove等功能,把ThreadLocalMap当成黑盒来看,关于ThreadLocalMap这样设计的原因我们后面再深入分析,先来看看ThreadLocal最核心的功能如何实现。
读写:转化为对Map的操作
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
拿到线程的ThreadLocalMap,调用getEntry方法,如果value为null设置为初始值并返回
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
同样转化为对线程的ThreadLocalMap的操作,如果没有Map就初始化。
还支持remove操作,也是一样的,就不贴源码了
自定义初始值:重写initialValue
get方法如果为null,ThreadLocal会返回一个可以自定义的初始值,自定义方法是initialValue
private T setInitialValue() {
T value = initialValue();
// 省略维护Map
return value;
}
protected T initialValue() {
return null;
}
我们可以通过重写initialValue方法从而设置ThreadLocal的资源的初始值
new ThreadLocal() {
@Override
protected MyResource initialValue() {
// 自定义初始值
return new MyResource();
}
};
绕过了ThreadLocalMap,实际上对ThreadLocal的实现原理就已经讲完了,但是:
但凡稍微背过点八股文的都知道:ThreadLocal是有「内存泄漏」的风险的。即ThreadLocalMap这个黑盒到底做了些什么事情,又有什么风险,要如何解决呢?
ThreadLocal的风险
1、脏数据
线程池一个很大的作用是「线程复用」,而线程复用,就会导致复用的线程共享同一个ThreadLocalMap,因此如果没有及时remove ThreadLocal,直接调用get而期望拿到initial value,很可能会拿到上个线程的ThreadLocal值,这种风险是致命的。
要解决也不难:
- 在线程执行完毕后,结束前,remove所有的ThreadLocal
- 保证所有线程使用ThreadLocal前,先set从而保证是自己的value
但第二种做法会导致initial value的失效,因此推荐remove方法
2、内存泄漏问题
我们先来看Map的键值对。
Entry:Map的键值对
static class Entry extends WeakReference
private T referent;
我们发现:Entry的key,也就是ThreadLocal,被设计成了弱引用。
弱引用:对象值得被回收
为什么key是弱引用
解决「内存泄露」问题。
内存泄露:对象没有被引用,已经是个无用的对象了,但是GC无法将该对象回收。
存在这样一条引用链:Thread -> ThreadLocalMap -> Entry -> ThreadLocal
因此,如果是强引用,即使没有任何引用指向ThreadLocal,只要线程不死亡,ThreadLocal 永远都不会死亡。因此,将key设计为弱引用,就可以解决这个问题。
但是Entry的value还是强引用,还是会发生「内存泄露」。即key为null,value还在。
value的内存泄漏
上图来自Richard_Yi,很好的阐述了value的内存泄漏问题
因此使用完ThreadLocal,需要手动调用remove()
回收。
ThreadLocal在执行get set remove方法都会尽量把ThreadLocal为null的entry回收掉,这是ThreadLocal尽力解决「内存泄漏」问题,但是无法彻底解决,还是要手动remove
题外话:ThreadLocal如何作为key
ThreadLocal类有个类变量:
private static final int HASH_INCREMENT = 0x61c88647;
所有ThreadLocal的实例对象有个threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
next也体现了ThreadLocal类的每个实例对象的threadLocalHashCode都尽量不相同,因此:与HashMap不同,HashMap利用HashCode方法获取HashCode,ThreadLocal额外维护了自己的threadLocalHashCode。
子线程获取父线程的ThreadLocal
很多场景下,ThreadLocal需要共享,子线程需要访问父线程的ThreadLocal,Java提供了一种优雅的方式:InheritableThreadLocal
。
当parent的inheritableThreadLocals
不为null
时,就会将parent
的inheritableThreadLocals
,赋值给当前线程的inheritableThreadLocals
。
InheritableThreadLocal实现原理
IheritableThreadLocal直接继承ThreadLocal,只写了三个方法:
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
在Java线程详解:线程模型,Thread类,异常处理器,异步执行结果Future一文中我们提到过,在创建线程时:
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
这个方法会将父线程的inheritableThreadLocals的所有键值对插入到自己的inheritableThreadLocals中。
InheritableThreadLocal与线程池
我们知道线程池的线程复用会带来脏数据问题,但ThreadLocal只需要手动remove即可解决,但InheritableThreadLocal就没这么好办了。
InheritableThreadLocal 无法解决,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况。
对于线程池中的线程,它们的InheritableThreadLocal
,都来自于线程池中第一个执行任务的线程,这就会导致InheritableThreadLocal
混乱/失效。
如何在线程池中正确的使用InheritableThreadLocal呢?TransmittableThreadLocal提供了这样的功能。transmittable-thread-local 是阿里巴巴开源的一个工具类,简称TTL,挖个坑,以后有空就来扒一扒TransmittableThreadLocal的源码。
参考文档
讲透 ThreadLocal 和 InheritableThreadLocal
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理