一文理解ThreadLocal
共 11082字,需浏览 23分钟
·
2021-06-28 08:30
本文讲解ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal。
有关本文的实验代码,可以查看文末补充:“比较一下ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在线程池复用线程的情况下的执行情况”。
ThreadLocal
ThreadLocal的使用场景
分布式跟踪系统
日志收集记录系统上下文
Session级Cache
应用容器或上层框架跨应用代码给下层SDK传递信息
举例:
Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
某些业务场景下,需要强制读主库来保证数据的一致性。在Sharding-JDBC中使用了ThreadLocal来存储相关配置信息,实现优雅的数据传递。
Spring Cloud Zuul用过滤器可以实现权限认证,日志记录,限流等功能,多个过滤器之间透传数据,底层使用了ThreadLocal。
在整个链路的日志中输出当前登录的用户ID,首先就得在拦截器获取过滤器中获取用户。ID,然后将用户ID进行存储到slf4j的MDC对象(底层使用ThreadLocal),然后进行链路传递打印日志。
ThreadLocal的结构
ThreadLocal的get()、set()方法,实际操作的都是Thread.currentThread(),即当前线程的threadLocals变量。
threadLocals变量包含了一个map成员变量(ThreadLocalMap)。
ThreadLocalMap的key为当前ThreadLocal, value为set的值。
相同的key在不同的散列表中的值必然是独立的,每个线程都是在各自的散列表中执行操作,如下图所示:
ThreadLocal的set方法:
public void set(T value) {
//currentThread是个native方法,会返回对当前执行线程对象的引用。
Thread t = Thread.currentThread();
//getMap 返回线程自身的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//把value set到线程自身的ThreadLocalMap中了
map.set(this, value);
} else {
//线程自身的ThreadLocalMap未初始化,则先初始化,再set
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal在set的时候,没有进行相应的深拷贝,所以ThreadLocal要想做线程隔离,必须是基本类型或者是Runable实现类的局部变量。
ThreadLocal造成内存泄漏
ThreadLocalMap内部Entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从代码中看到,Entry继承了WeakReference,并将ThreadLocal设置为了WeakReference,value设置为强引用。也就是:当没有强引用指向ThreadLocal变量时,它可被回收。
内存泄漏风险:ThreadLocalMap维护ThreadLocal变量与具体实例的映射,当ThreadLocal变量被回收后(变为null),无法路由到ThreadLocalMap。而该Entry还是在ThreadLocalMap中,从而这些无法清理的Entry,会造成内存泄漏。
所以,在使用ThreadLocal的时候,会话结束前务必使用ThreadLocal.remove方法(remove方法会将Entry的value及Entry自身设置为null并进行清理)。
ThreadLocal的最佳实践
ThreadLocal使用时必须显式地调用remove方法来避免内存泄漏。
ThreadLocal对象建议使用static修饰。这样做的好处是可以避免重复创建对象所导致的浪费(类第一次被使用时装载,只分配一块存储空间)。坏处是正好形成内存泄漏所需的条件(延长了ThreadLocal的生命周期,因此需要remove方法兜底)。
注释说明使用场景。
对性能有极致要求可以参考开源框架优化后的类,比如Netty的FastThreadLocal、Dubbo的InternalThreadLocal等。
InheritableThreadLocal
在全链路跟踪框架中,Trace信息的传递功能是基于ThreadLocal的。但实际业务中可能会使用异步调用,这样就会丢失Trace信息,破坏了链路的完整性。
此时可以使用JDK实现的InheritableThreadLocal,但它只支持父子线程间传递信息(例如:paramstream、new Thread等)。
Thread内部为InheritableThreadLocal开辟了一个单独的ThreadLocalMap(与ThreadLocal并列的成员变量)。在父线程创建一个子线程的时候,会检查这个ThreadLocalMap是否为空,不为空则会浅拷贝给子线程的ThreadLocalMap。
从类的继承层次来看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致。
Thread的init相关逻辑如下:
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
需要注意的是拷贝为浅拷贝。
TransmittableThreadLocal
InheritableThreadLocal可以在父线程创建子线程的时候将ThreadLocal中的值传递给子线程,从而完成链路跟踪框架中的上下文传递。
但大部分业务应用都会使用线程池,这种复用线程的池化场景中,线程池中的线程和主线程并不都是父子线程的关系,不能直接使用InheritableThreadLocal。
例如从Tomcat的线程(池化)提交task到业务线程池,就不能直接使用InheritableThreadLocal。
Transmittable ThreadLocal(简称TTL)是阿里开源的库,继承了InheritableThreadLocal,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。
TransmittableThreadLocal实现原理
InheritableThreadLocal不支持池化线程提交task到业务线程池的根本原因是,父线程创建子线程时,子线程InheritableThreadLocal只会复制一次环境变量。要支持线程池中能访问提交任务线程的本地变量,只需要在线程向线程池提交任务时复制父线程的上下环境,那在线程池中就能够访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传。
源码见于参考文档1,README有很详细的讲解,核心源码也不难,建议看看。
此外,项目引入TTL的时候,可以使用Java Agent植入修饰代码,修改runnable或者callable类,可以做到对应用代码无侵入(这个在README也有相关讲解)。
补充说明
ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在线程池复用线程的情况下的执行情况如下:
1.线程局部变量为基础类型
1.1 ThreadLocal
class TransmittableThreadLocalTest1 {
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(1);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[没有读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依旧读取的是 2
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
输出结果为:
主线程开启
主线程读取本地变量:1
子线程读取本地变量:null
主线程读取本地变量:2
子线程读取本地变量:null
子线程读取本地变量:3
主线程读取本地变量:2
1.2 InheritableThreadLocal
class TransmittableThreadLocalTest2 {
static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(1);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[没有读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依旧读取的是 2
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
输出结果为:
主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:1
子线程读取本地变量:3
主线程读取本地变量:2
1.3 TransmittableThreadLocal
class TransmittableThreadLocalTest3 {
static ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService executorService =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(1);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.set(2);
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.set(3);
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依旧读取的是 2
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
输出结果为:
主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:2
子线程读取本地变量:3
主线程读取本地变量:2
2.线程局部变量为类对象
首先定义一个数据类:
@Data
@AllArgsConstructor
class UserSession{
String uuid;
String nickname;
}
2.1 ThreadLocal
class TransmittableThreadLocalTest4 {
static ThreadLocal<UserSession> threadLocal = new ThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[没有读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//依旧读取的是 world
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
输出结果为:
主线程开启
主线程读取本地变量:UserSession(uuid=001, nickname=hello)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)
2.2 InheritableThreadLocal
class TransmittableThreadLocalTest5 {
static ThreadLocal<UserSession> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executorService =
Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//读取的是 Java(因为浅拷贝)
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
主线程开启
主线程读取本地变量:UserSession(uuid=001, nickname=hello)
子线程读取本地变量:UserSession(uuid=001, nickname=hello)
主线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:UserSession(uuid=001, nickname=Java)
主线程读取本地变量:UserSession(uuid=001, nickname=Java)
2.3 InheritableThreadLocal
class TransmittableThreadLocalTest6 {
static ThreadLocal<UserSession> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService executorService =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开启");
threadLocal.set(new UserSession("001","hello"));
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
threadLocal.get().setNickname("world");
System.out.println("主线程读取本地变量:" + threadLocal.get());
executorService.submit(() -> {
//[读到了主线程修改后的新值]
System.out.println("子线程读取本地变量:" + threadLocal.get());
threadLocal.get().setNickname("Java");
System.out.println("子线程读取本地变量:" + threadLocal.get());
});
TimeUnit.SECONDS.sleep(1);
//读取的是 Java(因为浅拷贝)
System.out.println("主线程读取本地变量:" + threadLocal.get());
}
}
输出结果与上面2.2的结果一样
参考文档:
https://github.com/alibaba/transmittable-thread-local