全链路跟踪(压测)必备基础组件之线程上下文“三剑客”

2024-05-24 14:38

本文主要是介绍全链路跟踪(压测)必备基础组件之线程上下文“三剑客”,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说起本地线程变量,我相信大家首先会想到的是JDK默认提供的ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的。由于本文的写作背景是笔者需要在公司落地全链路压测平台,一个基本并核心的功能需求是压测标记需要在整个调用链中进行传递,线程上下文环境成为解决这个问题最合适的技术。

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

ThreadLocal详解


ThreadLocal对外提供的API如下:

  • public T get()
    从线程上下文环境中获取设置的值。

  • public void set(T value)
    将值存储到线程上下文环境中,供后续使用。

  • public void remove()
    清除线程本地上下文环境。

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

1.1 ThreadLocal存储结构

图的几个关键点如下:
  • 数据存储
    当线程调用threadLocal对象的set(Object value)方法时,数据并不是存储在ThreadLocal对象中,而是存储在Thread对象中,这也是ThreadLocal的由来,具体存储在线程对象的threadLocals属性中,其类型为ThreadLocal.ThreadLocalMap。

  • ThreadLocal.ThreadLocalMap,Map结构,即键值对,键为threadLocal对象,值为需要存储到线程上下文的值(threadLocal#set)方法的参数。

1.2 源码分析ThreadLocal
1.2.1 源码分析get
 1public T get() {
2    Thread t = Thread.currentThread();  // @1
3    ThreadLocalMap map = getMap(t);  // @2
4    if (map != null) {                                // @3
5        ThreadLocalMap.Entry e = map.getEntry(this);
6        if (e != null) {
7            @SuppressWarnings("unchecked")
8            T result = (T)e.value;
9            return result;
10        }
11    }
12    return setInitialValue();  // @4
13}

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

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

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

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

ThreadLocal#setInitialValue

 1private T setInitialValue() {
2    T value = initialValue();    // @1
3    Thread t = Thread.currentThread();    // @2
4    ThreadLocalMap map = getMap(t);    // @3
5    if (map != null)                                     //@4
6       map.set(this, value);
7    else
8       createMap(t, value);                        // @5
9    return value;
10}

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

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

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

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

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

1.2.2 源码分析set
1public void set(T value) {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(this, value);
6    else
7        createMap(t, value);
8}

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

1.3 ThreadLocal局限性

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

 1public class Service {
2    private static ThreadLocal<Integer> requestIdThreadLocal = new ThreadLocal<>();
3    public static void main(String[] args) {
4        Integer reqId = new Integer(5);
5        Service a = new Service();
6        a.setRequestId(reqId);
7    }
8
9    public void setRequestId(Integer requestId) {
10        requestIdThreadLocal.set(requestId);
11        doBussiness();
12    }
13
14    public void doBussiness() {
15        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
16        (new Thread(new Runnable() {
17            @Override
18            public void run() {
19                System.out.println("子线程启动");
20                System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
21            }
22        })).start();
23    }
24}

运行结果如下:


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

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

InheritableThreadLocal


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

为了解决上述问题,JDK引入了InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,更严谨的说法是子线程可以访问在创建子线程时父线程当时的本地线程变量,因为其实现原理就是在创建子线程将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。
2.1 类图
从类的继承层次来看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致,故我们初步来看一下InheritableThreadLocal是如何重写上述这两个方法的。
1ThreadLocalMap getMap(Thread t) {
2    return t.inheritableThreadLocals;
3}
4void createMap(Thread t, T firstValue) {
5    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
6}

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

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

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

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

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

 1Thread parent = currentThread();                // @1
2
3// 省略部分代码
4
5if (inheritThreadLocals && parent.inheritableThreadLocals != null)    // @2
6    this.inheritableThreadLocals =
7        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
8/* Stash the specified stack size in case the VM cares */
9this.stackSize = stackSize;
10
11/* Set thread ID */
12tid = nextThreadID();

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

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

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

 1static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
2    return new ThreadLocalMap(parentMap);
3}
4private ThreadLocalMap(ThreadLocalMap parentMap) {
5    Entry[] parentTable = parentMap.table;
6    int len = parentTable.length;
7    setThreshold(len);
8    table = new Entry[len];
9
10    for (int j = 0; j < len; j++) {
11        Entry e = parentTable[j];
12        if (e != null) {
13            @SuppressWarnings("unchecked")
14            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
15            if (key != null) {
16                Object value = key.childValue(e.value);
17                Entry c = new Entry(key, value);
18                int h = key.threadLocalHashCode & (len - 1);
19                while (table[h] != null)
20                    h = nextIndex(h, len);
21
22        table[h] = c;
23                size++;
24            }
25        }
26    }
27}

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

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

2.3 验证InheritableThreadLocal的特性

验证代码如下:

 1public class Service {
2    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
3    public static void main(String[] args) {
4        Integer reqId = new Integer(5);
5        Service a = new Service();
6        a.setRequestId(reqId);
7    }
8
9    public void setRequestId(Integer requestId) {
10        requestIdThreadLocal.set(requestId);
11        doBussiness();
12    }
13
14    public void doBussiness() {
15        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
16        (new Thread(new Runnable() {
17            @Override
18            public void run() {
19                System.out.println("子线程启动");
20                System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
21            }
22        })).start();
23    }
24}

执行结果如下:

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

InheritableThreadLocal支持子线程访问在父线程中设置的线程上下文环境的实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。

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

 1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3public class Service {
4    /**
5     * 模拟tomcat线程池
6     */
7    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
8    /**
9     * 业务线程池,默认Control中异步任务执行线程池
10     */
11    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
12    /**
13     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
14     */
15    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
16
17    public static void main(String[] args) {
18
19        for(int i = 0; i < 10; i ++ ) {  // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
20                                                  //  然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
21            tomcatExecutors.submit(new ControlThread(i));
22        }
23
24        //简单粗暴的关闭线程池
25        try {
26            Thread.sleep(10000);
27        } catch (InterruptedException e) {
28            e.printStackTrace();
29        }
30        businessExecutors.shutdown();
31        tomcatExecutors.shutdown();
32    }
33
34
35    /**
36     * 模拟Control任务
37     */
38    static class ControlThread implements Runnable {
39        private int i;
40
41        public ControlThread(int i) {
42            this.i = i;
43        }
44        @Override
45        public void run() {
46         System.out.println(Thread.currentThread().getName() + ":" + i);
47                requestIdThreadLocal.set(i);
48                //使用线程池异步处理任务
49                businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
50        }
51    }
52
53    /**
54     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
55     */
56    static class BusinessTask implements Runnable {
57        private String parentThreadName;
58
59        public BusinessTask(String parentThreadName) {
60            this.parentThreadName = parentThreadName;
61        }
62
63        @Override
64        public void run() {
65            //如果与上面的能对应上来,则说明正确,否则失败
66            System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
67        }
68    }
69
70}

执行效果如下:

 1pool-1-thread-1:0
2pool-1-thread-2:1
3pool-1-thread-3:2
4pool-1-thread-4:3
5pool-1-thread-5:4
6pool-1-thread-6:5
7pool-1-thread-7:6
8pool-1-thread-8:7
9pool-1-thread-9:8
10pool-1-thread-10:9
11parentThreadName:pool-1-thread-7:6
12parentThreadName:pool-1-thread-4:6
13parentThreadName:pool-1-thread-3:6
14parentThreadName:pool-1-thread-2:6
15parentThreadName:pool-1-thread-1:6
16parentThreadName:pool-1-thread-9:6
17parentThreadName:pool-1-thread-10:6
18parentThreadName:pool-1-thread-8:7
19parentThreadName:pool-1-thread-6:5
20parentThreadName:pool-1-thread-5:4

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

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

怎么解决这个问题呢?

TransmittableThreadLocal ”闪亮登场“。

TransmittableThreadLocal


3.1 TransmittableThreadLocal“何许人也”

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

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

1<dependency>
2    <groupId>com.alibaba</groupId>
3    <artifactId>transmittable-thread-local</artifactId>
4    <version>2.10.2</version>
5</dependency>

示例代码如下:

 1public class Service {
2
3    /**
4     * 模拟tomcat线程池
5     */
6    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
7
8    /**
9     * 业务线程池,默认Control中异步任务执行线程池
10     */
11    private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl线程池,该框架的使用,请查阅官方文档。
12
13    /**
14     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
15     */
16   private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();
17
18//    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
19
20    public static void main(String[] args) {
21
22        for(int i = 0; i < 10; i ++ ) {
23            tomcatExecutors.submit(new ControlThread(i));
24        }
25
26        //简单粗暴的关闭线程池
27        try {
28            Thread.sleep(10000);
29        } catch (InterruptedException e) {
30            e.printStackTrace();
31        }
32        businessExecutors.shutdown();
33        tomcatExecutors.shutdown();
34
35    }
36
37
38    /**
39     * 模拟Control任务
40     */
41    static class ControlThread implements Runnable {
42        private int i;
43
44        public ControlThread(int i) {
45            this.i = i;
46        }
47        @Override
48        public void run() {
49            System.out.println(Thread.currentThread().getName() + ":" + i);
50            requestIdThreadLocal.set(i);
51
52            //使用线程池异步处理任务
53
54            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
55
56
57        }
58    }
59
60    /**
61     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
62     */
63    static class BusinessTask implements Runnable {
64        private String parentThreadName;
65
66        public BusinessTask(String parentThreadName) {
67            this.parentThreadName = parentThreadName;
68        }
69
70        @Override
71        public void run() {
72            //如果与上面的能对应上来,则说明正确,否则失败
73            System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
74        }
75    }
76
77}

其运行结果如下:

 1pool-1-thread-10:9
2pool-1-thread-8:7
3pool-1-thread-7:6
4pool-1-thread-9:8
5pool-1-thread-6:5
6pool-1-thread-5:4
7pool-1-thread-4:3
8pool-1-thread-3:2
9pool-1-thread-2:1
10pool-1-thread-1:0
11parentThreadName:pool-1-thread-5:4
12parentThreadName:pool-1-thread-9:8
13parentThreadName:pool-1-thread-3:2
14parentThreadName:pool-1-thread-2:1
15parentThreadName:pool-1-thread-7:6
16parentThreadName:pool-1-thread-8:7
17parentThreadName:pool-1-thread-1:0
18parentThreadName:pool-1-thread-6:5
19parentThreadName:pool-1-thread-10:9
20parentThreadName:pool-1-thread-4:3

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

3.2 TransmittableThreadLocal实现原理

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

3.2.1 TransmittableThreadLocal类图
TransmittableThreadLocal继承自InheritableThreadLocal,接下来将从set方法为入口,开始探究TransmittableThreadLocal实现原理。
3.2.2 set方法详解
1public final void set(T value) {
2    super.set(value);                              // @1
3    // may set null to remove value
4    if (null == value)                               // @2
5        removeValue();
6    else 
7    addValue();
8}

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

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

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

1private void addValue() {
2    if (!holder.get().containsKey(this)) {    // @1
3        holder.get().put(this, null); // WeakHashMap supports null value.
4    }
5}
6private void removeValue() {
7    holder.get().remove(this);
8}

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

 1private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
2            new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
3                @Override
4                protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
5                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>();
6                }
7
8                @Override
9                protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
10                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
11                }
12            };

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

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

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

 1ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4));
2TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
3parent.set("value-set-in-parent");
4Runnable task = new Task("1");
5Callable call = new Call("2");
6executorService.submit(task);
7executorService.submit(call);
8
9我们从submit为突破口,来尝试解开holder属性用途。
10class ExecutorTtlWrapper implements Executor, TtlEnhanced {
11    private final Executor executor;
12
13    ExecutorTtlWrapper(@Nonnull Executor executor) {
14        this.executor = executor;
15    }
16
17    @Override
18    public void execute(@Nonnull Runnable command) {
19        executor.execute(TtlRunnable.get(command));  // @1
20    }
21
22    @Nonnull
23    public Executor unwrap() {
24        return executor;
25    }
26}

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

3.2.2 TtlRunnable详解
3.2.2.1 类图
下面一一来介绍其核心属性:
  • AtomicReference capturedRef
    “捕获”的引用,根据下文的解读,该引用指向的数据结构包含了父线程在执行过程中,通过使用TransmittableThreadLocal存储的本地线程变量,但这里的触发时机是向线程池提交任务时捕获。

  • Runnable runnable
    提交到线程池中待执行的业务逻辑。

  • boolean releaseTtlValueReferenceAfterRun
    默认为false。

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

1private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
2    this.capturedRef = new AtomicReference<Object>(capture());   // @1
3    this.runnable = runnable;
4    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
5}

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

1TransmittableThreadLocal$Transmitter#capture
2public static Object capture() {
3    Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();  // @1
4    for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {     // @2
5        captured.put(threadLocal, threadLocal.copyValue());                              // @3
6    }
7    return captured;
8}

代码@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, ?>>。

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

3.2.2.2 run方法
 1public void run() {
2    Object captured = capturedRef.get();             
3    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
4        throw new IllegalStateException("TTL value reference is released after run!");
5    }
6
7    Object backup = replay(captured);                // @1
8    try {
9        runnable.run();                                           // @2
10    } finally {
11        restore(backup);                                        // @3
12    }
13}

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

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

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

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

3.2.2.3 replay
 1public static Object replay(@Nonnull Object captured) {
2    @SuppressWarnings("unchecked")
3    Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;      // @1
4    Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();              
5
6    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();               // @2
7                 iterator.hasNext(); ) {
8        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
9        TransmittableThreadLocal<?> threadLocal = next.getKey();
10
11        backup.put(threadLocal, threadLocal.get());                                                                                                                       // @3
12
13        // clear the TTL values that is not in captured
14        // avoid the extra TTL values after replay when run task
15        if (!capturedMap.containsKey(threadLocal)) {                                                                                                                    // @4
16            iterator.remove();
17            threadLocal.superRemove();
18        }
19     // set values to captured TTL
20      setTtlValuesTo(capturedMap);                                                                                                                                              // @5
21
22    // call beforeExecute callback
23    doExecuteCallback(true);                                                                                                                                                     // @6
24
25    return backup;                                                                                                                                                                       // @7
26}

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

  • capturedMap
    子线程从父线程捕获的线程本地遍历。

  • backup
    线程池中处理本次任务的线程中原先存在的本地线程变量。

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

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

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

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

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

3.2.2.4 restore

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

 1public static void restore(@Nonnull Object backup) {
2    @SuppressWarnings("unchecked")
3    Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;      // @1
4    // call afterExecute callback
5    doExecuteCallback(false);                                                                                                                                                   // @2
6
7    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();           // @3
8                 iterator.hasNext(); ) {
9        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
10        TransmittableThreadLocal<?> threadLocal = next.getKey();
11
12        // clear the TTL values that is not in bac1kup
13        // avoid the extra TTL values after restore
14        if (!backupMap.containsKey(threadLocal)) {                                                                                                                     // @4
15            iterator.remove();
16            threadLocal.superRemove();
17        }
18    }
19
20    // restore TTL values
21    setTtlValuesTo(backupMap);                                                                                                                                                  // @5
22} 

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

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

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

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

总结


本文介绍到这里了,详细介绍了ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal的实现原理,并从ThreadLocal、InheritableThreadLocal的局限性,最终引出TransmittableThreadLocal,为全链路压测中压测标记的透传打下坚实的基础。

相关文章:

Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失(上)

Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失(下)

这篇关于全链路跟踪(压测)必备基础组件之线程上下文“三剑客”的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/998696

相关文章

RedHat运维-Linux文本操作基础-AWK进阶

你不用整理,跟着敲一遍,有个印象,然后把它保存到本地,以后要用再去看,如果有了新东西,你自个再添加。这是我参考牛客上的shell编程专项题,只不过换成了问答的方式而已。不用背,就算是我自己亲自敲,我现在好多也记不住。 1. 输出nowcoder.txt文件第5行的内容 2. 输出nowcoder.txt文件第6行的内容 3. 输出nowcoder.txt文件第7行的内容 4. 输出nowcode

Vim使用基础篇

本文内容大部分来自 vimtutor,自带的教程的总结。在终端输入vimtutor 即可进入教程。 先总结一下,然后再分别介绍正常模式,插入模式,和可视模式三种模式下的命令。 目录 看完以后的汇总 1.正常模式(Normal模式) 1.移动光标 2.删除 3.【:】输入符 4.撤销 5.替换 6.重复命令【. ; ,】 7.复制粘贴 8.缩进 2.插入模式 INSERT

电脑不小心删除的文件怎么恢复?4个必备恢复方法!

“刚刚在对电脑里的某些垃圾文件进行清理时,我一不小心误删了比较重要的数据。这些误删的数据还有机会恢复吗?希望大家帮帮我,非常感谢!” 在这个数字化飞速发展的时代,电脑早已成为我们日常生活和工作中不可或缺的一部分。然而,就像生活中的小插曲一样,有时我们可能会在不经意间犯下一些小错误,比如不小心删除了重要的文件。 当那份文件消失在眼前,仿佛被时间吞噬,我们不禁会心生焦虑。但别担心,就像每个问题

零基础STM32单片机编程入门(一)初识STM32单片机

文章目录 一.概要二.单片机型号命名规则三.STM32F103系统架构四.STM32F103C8T6单片机启动流程五.STM32F103C8T6单片机主要外设资源六.编程过程中芯片数据手册的作用1.单片机外设资源情况2.STM32单片机内部框图3.STM32单片机管脚图4.STM32单片机每个管脚可配功能5.单片机功耗数据6.FALSH编程时间,擦写次数7.I/O高低电平电压表格8.外设接口

公共筛选组件(二次封装antd)支持代码提示

如果项目是基于antd组件库为基础搭建,可使用此公共筛选组件 使用到的库 npm i antdnpm i lodash-esnpm i @types/lodash-es -D /components/CommonSearch index.tsx import React from 'react';import { Button, Card, Form } from 'antd'

ps基础入门

1.基础      1.1新建文件      1.2创建指定形状      1.4移动工具          1.41移动画布中的任意元素          1.42移动画布          1.43修改画布大小          1.44修改图像大小      1.5框选工具      1.6矩形工具      1.7图层          1.71图层颜色修改          1

React+TS前台项目实战(十七)-- 全局常用组件Dropdown封装

文章目录 前言Dropdown组件1. 功能分析2. 代码+详细注释3. 使用方式4. 效果展示 总结 前言 今天这篇主要讲全局Dropdown组件封装,可根据UI设计师要求自定义修改。 Dropdown组件 1. 功能分析 (1)通过position属性,可以控制下拉选项的位置 (2)通过传入width属性, 可以自定义下拉选项的宽度 (3)通过传入classN

[FPGA][基础模块]跨时钟域传播脉冲信号

clk_a 周期为10ns clk_b 周期为34ns 代码: module pulse(input clk_a,input clk_b,input signal_a,output reg signal_b);reg [4:0] signal_a_widen_maker = 0;reg signal_a_widen;always @(posedge clk_a)if(signal_a)

00 - React 基础

1. React 基础 安装react指令 可参考: 官网官网使用教程 如: npx create-react-app 项目名如:npx create-react-app react-redux-pro JSX JSX 是一种 JavaScript 的语法扩展,类似于 XML 或 HTML,允许我们在 JavaScript 代码中编写 HTML。 const element =

Linux文本三剑客sed

sed和awk grep就是查找文本当中的内容,最强大的功能就是使用扩展正则表达式 sed sed是一种流编辑器,一次处理一行内容。 如果只是展示,会放在缓冲区(模式空间),展示结束后,会从模式空间把结果删除 一行行处理,处理完当前行,才会处理下一行。直到文件的末尾。 sed的命令格式和操作选项: sed -e '操作符 ' -e '操作符' 文件1 文件2 -e表示可以跟多个操作