线程上下文管理之“三剑客”--threadLocal

说起本地线程变量,我相信大家首先会想到的是 JDK 默认提供的 ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的。

由于本文的写作背景是笔者需要在公司落地全链路压测平台,一个基本并核心的功能需求是压测标记需要在整个调用链中进行传递,线程上下文环境成为解决这个问题最合适的技术。

温馨提示:本从从 ThreadLocal 原理入手分析,并抛出其缺点,再逐一引出InheritableThreadLocal、TransmittableThreadLocal。文章篇幅稍长,但由于循序渐进,层层递进,故精华部分在后面。

1、ThreadLocal 详解

ThreadLocal对外提供;的API如下:

  •  public T get()
    从线程上下文环境中获取设置的值。
  • public void set(T value)
    将值存储到线程上下文环境中,供后续使用。
  • public void remove()
    清除线程本地上下文环境。

上述API使用简单,关键是要理解 ThreadLocal 的内部存储结果。

1.1 ThreadLocal存储结构

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal 存储结构

上图的几个关键点如下:

  •  数据存储位置
    当线程调用 threadLocal 对象的 set(Object value) 方法时,数据并不是存储在 ThreadLocal 对象中,而是存储在 Thread 对象中,这也是 ThreadLocal 的由来,具体存储在线程对象的threadLocals 属性中,其类型为 ThreadLocal.ThreadLocalMap。
  •  ThreadLocal.ThreadLocalMap
    Map 结构,即键值对,键为 threadLocal 对象,值为需要存储到线程上下文的值(threadLocal#set)方法的参数。

1.2 源码分析 ThreadLocal

1.2.1 源码分析 get

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal get 方法

代码@1:获取当前线程。

代码@2:获取线程的 threadLocals 属性,在上图中已展示其存储结构。

代码@3:如果线程对象的 threadLocals 属性不为空,则从该 Map 结构中,用 threadLocal 对象为键去查找值,如果能找到,则返回其 value 值,否则执行代码@4。

代码@4:如果线程对象的 threadLocals 属性为空,或未从 threadLocals 中找到对应的键值对,则调用该方法执行初始化

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal#setInitialValue

代码@1:调用 initialValue() 获取默认初始化值,该方法默认返回 null,子类可以重写,实现线程本地变量的初始化。

代码@2:获取当前线程。

代码@3:获取该线程对象的 threadLocals 属性。

代码@4:如果不为空,则将 threadLocal:value 存入线程对象的 threadLocals 属性中。

代码@5:否则初始化线程对象的 threadLocals,然后将 threadLocal:value 键值对存入线程对象的threadLocals 属性中。

1.2.2 源码分析set

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal#set

在掌握了 get 方法实现细节,set 方法、remove 其实现的逻辑基本一样,就是对线程对象的threadLocals 属性进行操作( Map结构)。

1.3 ThreadLocal局限性

经过上面的剖析,对 ThreadLocal 的内部存储与 set、get、remove 等实现细节都已理解,但ThreadLocal 无法在父子线程之间传递,示例代码如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

父子传递 Demo

运行结果如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

Demo 示例结果

从结果上来看,在子线程中无法访问在父线程中设置的本地线程变量,那我们该如何来解决该问题呢?

为了解决该问题,JDK引入了另外一个线程本地变量实现类 InheritableThreadLocal,接下来将重点介绍 InheritableThreadLocal 的实现原理。

2、InheritableThreadLocal

由于 ThreadLocal 在父子线程交互中子线程无法访问到存储在父线程中的值,无法满足某些场景的需求,例如链路跟踪,例如如下场景:

全链路压测必备基础组件之线程上下文管理之“三剑客”

 

为了解决上述问题,JDK 引入了 InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,更严谨的说法是子线程可以访问在创建子线程时父线程当时的本地线程变量,因为其实现原理就是在创建子线程将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

2.1 类图

全链路压测必备基础组件之线程上下文管理之“三剑客”

InheritableThreadLocal 类图

从类的继承层次来看,InheritableThreadLocal 只是在 ThreadLocal 的 get、set、remove 流程中,重写了 getMap、createMap 方法,整体流程与 ThreadLocal 保持一致,故我们初步来看一下InheritableThreadLocal 是如何重写上述这两个方法的。

全链路压测必备基础组件之线程上下文管理之“三剑客”

InheritableThreadLocal#getMap

从代码得知,ThreadLocal 操作的是 Thread 对象的 threadLocals 属性,而 InheritableThreadLocal 操作的是 Thread 对象的 inheritableThreadLocals 属性。

温馨提示:createMap 被执行的条件是调用 InheritableThreadLocal#get、set 时如果线程的inheritableThreadLocals 属性为空时才会被调用。

那问题来了,InheritableThreadLocal 是如何继承自父对象的线程本地变量的呢?

2.2 线程上下文环境如何从父线程传递到子线程

这部分的代码入口为:Thread#init 方法

全链路压测必备基础组件之线程上下文管理之“三剑客”

Thread#init

子线程是通过在父线程中通过调用 new Thread() 方法来创建子线程,Thread#init 方法就是在 Thread的构造方法中被调用。

代码@1:获取当前线程对象,即待创建的线程的父线程。

代码@2:如果父线程的 inheritableThreadLocals 不为空并且 inheritThreadLocals 为 true(该值默认为true),则使用父线程的 inherit 本地变量的值来创建子线程的 inheritableThreadLocals 结构,即将父线程中的本地变量复制到子线程中。

全链路压测必备基础组件之线程上下文管理之“三剑客”

createInheritedMap

上述代码就不一一分析,类似于 Map 的复制,只不过其在 Hash 冲突时,不是使用链表结构,而是直接在数组中找下一个为 null 的槽位。

温馨提示:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承 InheritableThreadLocal 并重写 childValue 方法。

2.3 验证 InheritableThreadLocal 的特性

验证代码如下:

public class Service {

private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

Integer reqId = new Integer(5);

Service a = new Service();

a.setRequestId(reqId);

}

public void setRequestId(Integer requestId) {

requestIdThreadLocal.set(requestId);

doBussiness();

}

public void doBussiness() {

System.out.println("首先打印requestId:" + requestIdThreadLocal.get());

(new Thread(new Runnable() {

@Override

public void run() {

System.out.println("子线程启动");

System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());

}

})).start();

}

}

执行结果如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

 

符合预期,在子线程中如愿访问到了在主线程中设置的本地环境变量。

2.4 InheritableThreadLocal 局限性

InheritableThreadLocal 支持子线程访问在父线程的核心思想是在创建线程的时候将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。但我们提到并发、多线程就理不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱,验证代码如下:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class Service {

/**

* 模拟tomcat线程池

*/

private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

/**

* 业务线程池,默认Control中异步任务执行线程池

*/

private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);

/**

* 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。

*/

private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

for(int i = 0; i < 10; i ++ ) { // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,

// 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量

tomcatExecutors.submit(new ControlThread(i));

}

//简单粗暴的关闭线程池

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

businessExecutors.shutdown();

tomcatExecutors.shutdown();

}

/**

* 模拟Control任务

*/

static class ControlThread implements Runnable {

private int i;

public ControlThread(int i) {

this.i = i;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + ":" + i);

requestIdThreadLocal.set(i);

//使用线程池异步处理任务

businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));

}

}

/**

* 业务任务,主要是模拟在Control控制层,提交任务到线程池执行

*/

static class BusinessTask implements Runnable {

private String parentThreadName;

public BusinessTask(String parentThreadName) {

this.parentThreadName = parentThreadName;

}

@Override

public void run() {

//如果与上面的能对应上来,则说明正确,否则失败

System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());

}

}

}

执行效果如下:

pool-1-thread-1:0

pool-1-thread-2:1

pool-1-thread-3:2

pool-1-thread-4:3

pool-1-thread-5:4

pool-1-thread-6:5

pool-1-thread-7:6

pool-1-thread-8:7

pool-1-thread-9:8

pool-1-thread-10:9

parentThreadName:pool-1-thread-7:6

parentThreadName:pool-1-thread-4:6

parentThreadName:pool-1-thread-3:6

parentThreadName:pool-1-thread-2:6

parentThreadName:pool-1-thread-1:6

parentThreadName:pool-1-thread-9:6

parentThreadName:pool-1-thread-10:6

parentThreadName:pool-1-thread-8:7

parentThreadName:pool-1-thread-6:5

parentThreadName:pool-1-thread-5:4

从这里可以出 thread-7、thread-4、thread-3、thread-2、thread-1、thread-9、thread-10 获取的都是6,在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。

问题:大家通过上面的学习,应该能解释这个现象?此处可以稍微停下来思考一番。

怎么解决这个问题呢?

TransmittableThreadLocal ”闪亮登场“。

3、TransmittableThreadLocal

3.1 TransmittableThreadLocal“何许人也”

TransmittableThreadLocal 何许人也,它可是阿里巴巴开源的专门解决 InheritableThreadLocal 的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。实践是检验整理的唯一标准,我们还是以上面的示例来进行验证,看看 TransmittableThreadLocal 是否支持上述场景:

首先需要在 pom.xml 文件中引入如下 maven 依赖:

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>transmittable-thread-local</artifactId>

<version>2.10.2</version>

</dependency>

示例代码如下:

public class Service {

/**

* 模拟tomcat线程池

*/

private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

/**

* 业务线程池,默认Control中异步任务执行线程池

*/

private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl线程池,该框架的使用,请查阅官方文档。

/**

* 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。

*/

private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();

// private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

for(int i = 0; i < 10; i ++ ) {

tomcatExecutors.submit(new ControlThread(i));

}

//简单粗暴的关闭线程池

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

businessExecutors.shutdown();

tomcatExecutors.shutdown();

}

/**

* 模拟Control任务

*/

static class ControlThread implements Runnable {

private int i;

public ControlThread(int i) {

this.i = i;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + ":" + i);

requestIdThreadLocal.set(i);

//使用线程池异步处理任务

businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));

}

}

/**

* 业务任务,主要是模拟在Control控制层,提交任务到线程池执行

*/

static class BusinessTask implements Runnable {

private String parentThreadName;

public BusinessTask(String parentThreadName) {

this.parentThreadName = parentThreadName;

}

@Override

public void run() {

//如果与上面的能对应上来,则说明正确,否则失败

System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());

}

}

}

其运行结果如下:

pool-1-thread-10:9
pool-1-thread-8:7
pool-1-thread-7:6
pool-1-thread-9:8
pool-1-thread-6:5
pool-1-thread-5:4
pool-1-thread-4:3
pool-1-thread-3:2
pool-1-thread-2:1
pool-1-thread-1:0
parentThreadName:pool-1-thread-5:4
parentThreadName:pool-1-thread-9:8
parentThreadName:pool-1-thread-3:2
parentThreadName:pool-1-thread-2:1
parentThreadName:pool-1-thread-7:6
parentThreadName:pool-1-thread-8:7
parentThreadName:pool-1-thread-1:0
parentThreadName:pool-1-thread-6:5
parentThreadName:pool-1-thread-10:9
parentThreadName:pool-1-thread-4:3

执行结果符合预期。那TransmittableThreadLocal是如何实现的呢?

3.2 TransmittableThreadLocal实现原理

从InheritableThreadLocal 不支持线程池的根本原因是 InheritableThreadLocal 是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程在向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地遍历,实现本地环境变量在线程调用之中的透传,实现链路跟踪,这也就是 TransmittableThreadLocal 最本质的实现原理。

3.2.1 TransmittableThreadLocal 类图

全链路压测必备基础组件之线程上下文管理之“三剑客”

TransmittableThreadLocal类图

TransmittableThreadLocal 继承自 InheritableThreadLocal,接下来将从 set 方法为入口,开始探究TransmittableThreadLocal 实现原理。

3.2.2 set 方法详解

全链路压测必备基础组件之线程上下文管理之“三剑客”

TransmittableThreadLocal #set

代码@1:首先调用父类的 set 方法,将 value 存入线程本地遍历,即 Thread 对象的inheritableThreadLocals 中。

代码@2:如果 value 为空,则调用 removeValue() 否则调用 addValue。

那接下来重点看看这两个方法有什么名堂:

全链路压测必备基础组件之线程上下文管理之“三剑客”

TransmittableThreadLocal#addValue

代码@1:当前线程在调用 threadLocal 方法的 set 方法(即向线程本地遍历存储数据时),如果需要设置的值不为 null,则调用 addValue 方法,将当前 ThreadLocal 存储到 TransmittableThreadLocal 的全局静态变量 holder。holder 的定义如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

 

从中可以看出,使用了线程本地变量,内部存放的结构为 Map<TransmittableThreadLocal<?>, ?>,即该对象缓存了线程执行过程中所有的 TransmittableThreadLocal 对象,并且其关联的值不为空。但这样做有什么用呢?

为了解开这个难题,可能需要大家对 ttl 这个框架的使用有一定的理解,本文由于篇幅的原因,将不会详细介绍,如有大家有兴趣,可以查阅其官网了解其使用:
https://github.com/alibaba/transmittable-thread-local

全链路压测必备基础组件之线程上下文管理之“三剑客”

ttl使用示例

我们从submit 为突破口,来尝试解开 holder 属性用途。

全链路压测必备基础组件之线程上下文管理之“三剑客”

 

在向线程池提交任务时,会使用 TtlRunnable 对提交任务进行包装。接下来将重点探讨 TtlRunnable。

3.2.2 TtlRunnable详解

3.2.2.1 类图

全链路压测必备基础组件之线程上下文管理之“三剑客”

TtlRunnable类图

下面一一来介绍其核心属性:

  •  AtomicReference< Object> capturedRef
    “捕获”的引用,根据下文的解读,该引用指向的数据结构包含了父线程在执行过程中,通过使用TransmittableThreadLocal 存储的本地线程变量,但这里的触发时机是向线程池提交任务时捕获。
  •  Runnable runnable
    提交到线程池中待执行的业务逻辑。
  •  boolean releaseTtlValueReferenceAfterRun
    默认为false。

接下来重点看一下其构造方法

全链路压测必备基础组件之线程上下文管理之“三剑客”

TtlRunnable构造函数

构造方法没什么特别,重点看一下子线程是如何“捕获”父线程中已设置的本地线程变量。

全链路压测必备基础组件之线程上下文管理之“三剑客”

TransmittableThreadLocal$Transmitter#capture

代码@1:先创建 Map 容器,用来存储父线程的本地线程变量,键为在父线程执行过程中使用到的TransmittableThreadLocal 线程。

代码@2:holder.get(),获取父线程中使用中的 ThreadLocal,因为我们从3.2.2节中发现,在当前线程在调用 TransmittableThreadLocal 的 set 方法,并且其值不为空的时候,会将TransmittableThreadLocal 对象存储存储在当前线程的本地变量中。故这里使用 holder.get() 方法能获取父线程中已使用的 ThreadLocal,并其值不为null。

代码@3:遍历父线程已使用的线程本地,将其值存入到 captured 中,注意默认是浅拷贝,如果想实现深度拷贝,请重写 TransmittableThreadLocal 的 copyValue 方法。

温馨提示:从这里看出 TransmittableThreadLocal 的静态属性 holder 的用处吧,请重点理解holder 的属性类型为:InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>。

在向线程池提交任务时,就会先捕获父线程(提交任务到线程池的线程)中的本地环境变量,接下来重点来看一下其run方法。

3.2.2.2 run方法

全链路压测必备基础组件之线程上下文管理之“三剑客”

TtlRunnable#run

代码@1:"重放"父线程的本地环境变量,即使用从父线程中捕获过来的上下文环境,在子线程中重新执行一遍,并返回原先存在与子线程中的上下文环境变量。

代码@2:执行业务逻辑。

代码@3:恢复线程池中当前执行任务的线程的上下文环境,即代码@1,会直接继承父线程中的上下文环境,但会将原先存在该线程的线程上下文环境进行备份,在任务执行完后通过执行restore方法进行恢复。

不得不佩服这里设计的巧妙。笔者有理由相信能看到这里的诸位读者一定是有实力有求知的欲望的人,那我们再来看一下replay、restore方法的实现。

3.2.2.3 replay

全链路压测必备基础组件之线程上下文管理之“三剑客”

TtlRunnable#replay

代码@1:首先解释一下两个局部变量的含义:

  •  capturedMap
    子线程从父线程捕获的线程本地遍历。
  • backup
    线程池中处理本次任务的线程中原先存在的本地线程变量。

代码@2:holder.get(),这里是子线程中原先存在的本地线程变量(即线程池中分配来执行本次任务的线程),然后遍历它,将其存储在backUp(@3)。

代码@4:从这里开始,开始将根据父线程的本地变量来重放当前线程,如果父线程中不包含的threadlocal 对象,将从本地线程变量中移除。

代码@5:遍历父线程中的本地线程变量,在子线程中重新执行一次 threadlocal.set 方法。

代码@6:执行 beforeExecute() 狗子函数。

代码@7:返回线程池原线程的本地线程变量,供本次调用后恢复上下文环境。

3.2.2.4 restore

恢复线程中子线程原先的本地线程变量,即恢复线程,本次执行并不会污染线程池中线程原先的上下文环境,精妙。我们来看看其代码实现:

全链路压测必备基础组件之线程上下文管理之“三剑客”

TtlRunnable#restore

代码@1:获取备份好的线程本地上下文。

代码@2:执行 afterExecute() 钩子函数。

代码@3:遍历本地线程变量,将不属于 backUpMap 中存在的线程本地上下文移除(@4)。

代码@5:遍历备份的本地线程本地,在本地线程中重新执行 threadlocal#set 方法,实现线程本地变量的还原。

posted @ 2021-06-23 16:01  菜菜聊架构  阅读(549)  评论(0编辑  收藏  举报