原文:
说起本地线程变量,我相信大家首先会想到的是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 ThreadLocalrequestIdThreadLocal = 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
上述代码就不一一分析,类似于Map的复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位。
温馨提示:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承InheritableThreadLocal并重写childValue方法。
2.3 验证InheritableThreadLocal的特性
验证代码如下:
1public class Service { 2 private static InheritableThreadLocalrequestIdThreadLocal = 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 InheritableThreadLocalrequestIdThreadLocal = 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,在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。
问题:大家通过上面的学习,应该能解释这个现象?此处可以稍微停下来思考一番。
怎么解决这个问题呢?
我们来看看InheritableThreadLocal是怎么解决子线程复制父线程的本地变量的.我们看看new Thread() 是怎么实现的:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
我们 可以发现在new Thread() 过程中,会把父线程inheritableThreadLocals 复制到 子线程inheritableThreadLocals 中达到本地变量的传递。
在用线程池时,我们只传递了Runnable
和Callable,所以我们要实现 线程池的本地变量复用就需要 实现 Runnable或 Callable,在实现中记录父线程中的本地变量,在线程运行时,把父线程的本地变量复制给子线程。我们可以看看阿里工程师是怎么做的,
TransmittableThreadLocal ”闪亮登场“。
TransmittableThreadLocal
3.1 TransmittableThreadLocal“何许人也”
TransmittableThreadLocal何许人也,它可是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。实践是检验整理的唯一标准,我们还是以上面的示例来进行验证,看看TransmittableThreadLocal是否支持上述场景:
首先需要在pom.xml文件中引入如下maven依赖:
12 com.alibaba 3transmittable-thread-local 42.10.2 5
示例代码如下:
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 TransmittableThreadLocalrequestIdThreadLocal = new TransmittableThreadLocal<>(); 17 18// private static InheritableThreadLocal 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对象,并且其关联的值不为空。但这样做有什么用呢?
为了解开这个难题,可能需要大家对ttl这个框架的使用有一定的理解,本文由于篇幅的原因,将不会详细介绍,如有大家有兴趣,可以查阅其官网解其使用:
https://github.com/alibaba/transmittable-thread-local
1ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); 2TransmittableThreadLocalparent = new TransmittableThreadLocal (); 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(capture()); // @1 3 this.runnable = runnable; 4 this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; 5}
构造方法没什么特别,重点看一下子线程是如何“捕获”父线程中已设置的本地线程变量。
1TransmittableThreadLocal$Transmitter#capture 2public static Object capture() { 3 Map, Object> captured = new HashMap , 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, Object> capturedMap = (Map , Object>) captured; // @1 4 Map , Object> backup = new HashMap , Object>(); 5 6 for (Iterator , ?>> iterator = holder.get().entrySet().iterator(); // @2 7 iterator.hasNext(); ) { 8 Map.Entry , ?> 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, Object> backupMap = (Map , Object>) backup; // @1 4 // call afterExecute callback 5 doExecuteCallback(false); // @2 6 7 for (Iterator , ?>> iterator = holder.get().entrySet().iterator(); // @3 8 iterator.hasNext(); ) { 9 Map.Entry , ?> 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,为全链路压测中压测标记的透传打下坚实的基础。
各位可爱的读者,您能看到这里,我相信应该收获满满,有劳帮忙点个赞,谢谢您的鼓励。
本文重点是关注ThreadLocal的应用场景,对于性能方面未曾关注,其实在Netty的实现中专门为了优化ThreadLocal访问的性能,推出了FastThreadLocal实现,如果有兴趣,可以查阅笔者的另外一篇博文:ThreadLocal原理分析与性能优化思考: