ThreadLocal, InheritableThreadLocal 以及 TransmittableThreadLocal

ThreadLocal 是 Java 编程人员要掌握的一个基本类,似乎没什么太多要说。但因为本文要牵出 TransmittableThreadlLocal, 再顺带说下几乎隐形的 InheritableThreadLocal。

ThreadLocal 用于保存与线程绑定的数据,它在框架内部使用的很频繁,但凡见到 XxxContextHolder.currentContext() 之类的十之八九用到了 ThreadLocal, 如 Spring 框架中的

RequestContextHolder

在 Spring Web 项目任何代码中,只要是由 http 线程执行都可以用 RequestContextHolder.currentRequestAttributes() 得到请求中的属性值,由此能追溯到 Session, Application 等对象

SecurityContextHolder 中的 SecurityContextHolderStrategy 实现方式有 ThreadLocalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy 等

在 SpringSecurity 的 Filter 中调用静态方法可得到 Context 并设置 Authentication, SecurityContextHolder.getContext().setAuthentication(), 在后续的调用链上就能通过静态方法获得它

这也是为什么在 Spring 中可以用 @Transactional 声明实现事物关键,在任何代码能取到当前线程所用的数据库连接,从而把需要的数据库操作放在同一个事物

还有日志的 MDC 也是用的 ThreadLocal, 对了上面还有 ThreadLocal 的直接子类 NamedThreadLocal, 它的功能上没多少特别的。如果在一个 Spring 项目中查看 ThreadLocal 的类层次将能列出更多的  ThreadLocal 实现类。

觉察到 TheadLocal 在框架内部如此广泛的被使用,我们来大略回顾一下 ThreadLocal 的基本用法

ThreadLocal 唯的作用就是保存在其中的值是与当前线程相绑定的,与 ThreadLocal 变量本生声明在何处无关。看下面的代码

输出为

Thread-0=null
main=1

即在主 main 线程中写入到 ThreadLocal 中的值只对当前 main 线程可见,对其他线程不可见,虽然它们都能访问 ThreadLocal value 变量,但存储与 value 关联的值是在线程上。

换句话说就是 ThreadLocal 中的值能随着当前线程流动。

如果当前线程 ThreadLocal 想要它其中的值传播到子线程是否可行呢?答案是 InheritableThreadLocal,它满足了我们部分需求

只需要把上面的 new ThreadLocal<>() 改成 new InheritableThreadLocal<>() 就能产生不一样的效果,列出完整代码

现在的输出变为

Thread-0=1
main=1

这符合我们的预期,使用了 InheritableThreadLocal,在 main 线程中用 new Thread() 创建子线程时会把 main 线程(父线程)中绑定的值传递给子线程,因为如果是 InheritableThreadLocal 的话在 Thread 的构造函数中会从父线程拷贝线程局部变量到子线程中,相关代码如下

JDK 中添加 InheritableThreadLocal 这个子类的使用前提应该是新线程只使用用一次,不被重用,也就是说 new Thread().start() 完成任务后线程的状态即变为 Terminated, 不能被复用。

那如何能复用这个线程呢?线程执行完任务后要转为 wait 状态,然后等着新任务到来后唤醒它,下面是一个简陋的实现

TaskRunner 更像是一个单线程的线程池,实质也确实是。以上代码可以采用更高效的锁 java.util.concurrent.locks.ReetrantLock 和 java.util.concurrent.locks.Condition 来进行代码同步,或进一步演化成借助于 BlockingQueue 的 offer, take 天然的锁,等待方式实现,这恰好是朝着线程池进化的方向走。

应用该 TaskRunner

输出为

main=1
Thread-0=1
Thread-0=1

在 value.set(2) 修改为 2 后,子线程 Thread-0 中的 value 仍然是 1,因为线程被复用了,前面说过只有第一次 new Thread(...) 的时候才从父线程拷贝线程局部变量到子线程,而后父线程中的线程局部变量 value 修改之后并不会同步到子线程的的线程局部变量去。

所以说 InheritableThreadLocal 碰上了可复用的线程或者说线程池就失去功效了,然而如今又正是各种线程池大行其道的时候,感觉 InheritableThreadLocal 还没真正上路就要退出历史的舞台。而且你在线程池中一不留心还会带来困惑,比如下面用 InheritableThreadLocal 与线程池的代码

输出

main=1
pool-1-thread-1=1
main=2
pool-1-thread-2=2
main=3
pool-1-thread-3=3
main=4
pool-1-thread-4=4
main=5
pool-1-thread-5=5

乍一看,似乎 InheritableThreadLocal 满足了我们的需求,外面设置的 value 立即能反应到子线程中

如果再来一趟呢

输出

main=1
pool-1-thread-1=1
main=2
pool-1-thread-2=2
main=3
pool-1-thread-3=3
main=4
pool-1-thread-4=4
main=5
pool-1-thread-5=5
------------
main=6
pool-1-thread-1=1
main=7
pool-1-thread-2=2
main=8
pool-1-thread-3=3
main=9
pool-1-thread-4=4
main=10
pool-1-thread-5=5

第二遍的时候不管外面线程对 value 设置任何值,子线程依然保持最初始的值,原因是一样,线程只有在 new Thread(...) 时才会从父线程上拷贝 InheritableThreadLocal 上的值,线程池中的线程是在等到要跑任务时才创建,所以正好第一轮每次外面改变 InheritableThreadLocal 上的值就为线程池创建一个新线程来执行当前任务,所以 InheritableThreadLocal 产生了效果,第二轮一概复用线程,无需 new Thread(...) 的过程,所以子线程中的线程局部对外部 InheritableThreadLocal 的变化就无动于衷了。

那结论是什么呢?就是不要 InheritableThreadLocal 和线程池搭配使用。

TransmittableThreadLocal 解决线程池与 TheadLocal 的问题

又是 Alibaba 出了一款 ThreadLocal 子类实现 ThansmittableThreadLocal, 意图是为了解决 InheritableThreadLocal 与线程池一同工作的的问题,大概看了一下,也没什么特别的。起初还以为是直接把上面的

new InheritableThreadLocal() 替换成 new TransmittableThreadLocal() 就万事大吉了,到底还没透明到这一程度,这种简单换没有任何帮助。

ThransmittableThreadLocal 同样的是走的主流路线,三种方案

  1. 修饰 Task
  2. 修饰线程池
  3. 以 -javaagent:path/to/transmittable-thread-local-2.x.y.jar 的方式动态修改字节码

需要在项目中引入依赖

第一种方式与 Spring 的 ThreadPoolTaskExecutor 的 setTaskDecorator(taskDecorator) 是类似的实现(目前项目中采取的就是 TaskDecorator,比如要传递 MDC 的 TaskDecorator)

它实现了对多种函数接口的封装,如 TtlRunable, TtlCallable, TtlFunction, TtlConsumer 等,注意 Ttl 不是 Time To Live 的意思,而是 TransmittableThreadLocal 的缩写,实现方类与 TaskDecorator 类同

装饰任务

以 TtlRunable 为例

输出

main=1
pool-1-thread-1=1
main=2
pool-1-thread-2=2
main=3
pool-1-thread-1=3
main=4
pool-1-thread-2=4
main=5
pool-1-thread-1=5

外面作什么改变,线程池中的线程也立即作出反应。

装饰 ThreadPool

TtlExecutors 有 getTtlExecutor(), getTtlExecutorService(), getTtlScheduledExecutorService() 等线程池的装饰器,上面代码改造成用 TtlExecutors.getTtlExecutor()

也是输出

main=1
pool-1-thread-1=1
main=2
pool-1-thread-2=2
main=3
pool-1-thread-1=3
main=4
pool-1-thread-2=4
main=5
pool-1-thread-1=5

-javaagent:path/to/transmittable-thread-local-2.x.y.jar 方式

代码中仍然要用 TransmittableThreadLocal 替代 InheritableThreadLocal, 所以项目还是要 transmittable-thread-local 的依赖,算不上彻底的无侵入性

只是不用装饰 task 或者线程池,运行时指定 -javaagent

java -javaagent:/Users/yanbin/.m2/repository/com/alibaba/transmittable-thread-local/2.14.5/transmittable-thread-local-2.14.5.jar blog.yanbin.TestThread
main=1
pool-1-thread-1=1
main=2
pool-1-thread-2=2
main=3
pool-1-thread-1=3
main=4
pool-1-thread-2=4
main=5
pool-1-thread-1=5

由于在 -javaagent 中包含了 transmittable-thread-local-2.14.5.jar, 所以用不着再把它加到 classpath 中去。

在线程池中传递 ThreadLocal 可能的问题

在阅读 Transmittable ThreadLocal 的 使用 TTL 的好处与必要性 一节要留意上下文丢失的两种情况

  • 当线程池满了且线程池的RejectedExecutionHandler使用的是CallerRunsPolicy时,提交到线程池的任务会在提交线程中直接执行,ThreadLocal.remove操作清理提交线程的上下文导致上下文丢失
  • 类似的,使用ForkJoinPool(包含并行执行StreamCompletableFuture,底层使用ForkJoinPool)的场景,展开的ForkJoinTask会在任务提交线程中直接执行。同样导致上下文丢失

解决办法可以在完成任务后不清理上下文,而在下次得胜该线程时再进行覆盖。

本文链接 https://yanbin.blog/threadlocal-inheritablethreadlocal-%e4%bb%a5%e5%8f%8a-transmittablethreadlocal/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments