一文理解ThreadLocal

共 11082字,需浏览 23分钟

 ·

2021-06-28 08:30

本文讲解ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal。

有关本文的实验代码,可以查看文末补充:“比较一下ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在线程池复用线程的情况下的执行情况”。

ThreadLocal

ThreadLocal的使用场景

  1. 分布式跟踪系统

  2. 日志收集记录系统上下文

  3. Session级Cache

  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

举例:

  1. Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

  2. 某些业务场景下,需要强制读主库来保证数据的一致性。在Sharding-JDBC中使用了ThreadLocal来存储相关配置信息,实现优雅的数据传递。

  3. Spring Cloud Zuul用过滤器可以实现权限认证,日志记录,限流等功能,多个过滤器之间透传数据,底层使用了ThreadLocal。

  4. 在整个链路的日志中输出当前登录的用户ID,首先就得在拦截器获取过滤器中获取用户。ID,然后将用户ID进行存储到slf4j的MDC对象(底层使用ThreadLocal),然后进行链路传递打印日志。

ThreadLocal的结构

  1. ThreadLocal的get()、set()方法,实际操作的都是Thread.currentThread(),即当前线程的threadLocals变量。

  2. threadLocals变量包含了一个map成员变量(ThreadLocalMap)。

  3. 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的最佳实践

  1. ThreadLocal使用时必须显式地调用remove方法来避免内存泄漏。

  2. ThreadLocal对象建议使用static修饰。这样做的好处是可以避免重复创建对象所导致的浪费(类第一次被使用时装载,只分配一块存储空间)。坏处是正好形成内存泄漏所需的条件(延长了ThreadLocal的生命周期,因此需要remove方法兜底)。

  3. 注释说明使用场景。

  4. 对性能有极致要求可以参考开源框架优化后的类,比如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的结果一样

参考文档:

  1. https://github.com/alibaba/transmittable-thread-local


浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报