1.InheritableThreadLocal的不足
之前我们就总结过父子线程之间上下文如何传递:父子线程之间值传递解决方案:InheritableThreadLocal和TransmittableThreadLocal
重点介绍了InheritableThreadLocal(后文简写ITL)
概念、实现原理、使用示例和不足,同时浅浅地引出了TransmittableThreadLocal(后文简称TTL)
的使用,但并没有展开详细分析,今天我们就来重点总结下TTL
JDK
的ITL
类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的。而ITL
是在父线程调用new Thread()
方法来创建子线程时,执行Thread#init
方法,把父线程ITL
数据拷贝到子线程中。所以线程池的线程只会创建一次触发ITL
的拷贝逻辑,以后都是池化起来复用,这时候执行上下文就不能在父子线程中正常传递了,示例如下:
csharp
// 创建线程池
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
context.set("value1-set-in-parent");
fixedThreadPool.submit(()->{
// 第一个子线程打印value1-set-in-parent,毋庸置疑
System.out.println(context.get());
});
// 父线程修改InheritableThreadLocal变量值
context.set("value2-set-in-parent");
fixedThreadPool.submit(()->System.out.println(context.get()));
}
执行结果:两次都是value1-set-in-parent
arduino
value1-set-in-parent
value1-set-in-parent
代码解读:
Executors.newFixedThreadPool(1)
: 创建只有一个线程的线程池- 两次异步执行,在执行前设置了
ITL
的值,但线程池只有一个线程,第二次异步执行其实是复用线程,并没有新创建线程,这时候ITL
的变量值就是前面那个线程的值,所以这里和第一个子线程一样打印value1-set-in-parent,如果上面线程池核心数和最大线程数都为2,这时候会新建一个子线程,此时会获取父线程的最新值,打印value2-set-in-parent
2.TransmittableThreadLocal使用
引入依赖:
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.6</version>
</dependency>
2.1 修饰Runnable
使用TTL
, 并用TtlRunnable.get()
修饰Runnable
,完美解决上面问题
csharp
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
// ttl
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>();
context.set("value1-set-in-parent");
fixedThreadPool.submit(()->{
// 第一个子线程打印value1-set-in-parent,毋庸置疑
System.out.println(context.get());
});
// 父线程修改InheritableThreadLocal变量值
context.set("value2-set-in-parent");
Runnable runnable = ()->System.out.println(context.get());
TtlRunnable ttlRunnable = TtlRunnable.get(runnable);
fixedThreadPool.submit(ttlRunnable);
}
即使是同一个Runnable
任务多次提交到线程池时,每次提交时都需要通过修饰操作(即TtlRunnable.get(task)
)以抓取这次提交时的TransmittableThreadLocal
上下文的值;即如果同一个任务下一次提交时不执行修饰而仍然使用上一次的TtlRunnable
,则提交的任务运行时会是之前修饰操作所抓取的上下文。示例代码如下:
csharp
// 第一次提交
Runnable task = new RunnableTask();
executorService.submit(TtlRunnable.get(task));
// ...业务逻辑代码,
// 并且修改了 TransmittableThreadLocal上下文 ...
context.set("value-modified-in-parent");
// 再次提交
// 重新执行修饰,以传递修改了的 TransmittableThreadLocal上下文
executorService.submit(TtlRunnable.get(task));
为了省去每次Runnable
和Callable
传入线程池时的修饰,这个逻辑可以在线程池中完成
2.2 修饰线程池
如上2.1效果,使用工具类TtlExecutors
修饰线程池,这样解决上面ITL
的线程池线程复用值传递问题
csharp
// 创建线程池,并于ttl修饰
private static ExecutorService fixedThreadPool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
public static void main(String[] args) {
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>();
context.set("value1-set-in-parent");
fixedThreadPool.submit(()->{
// 第一个子线程打印value1-set-in-parent,毋庸置疑
System.out.println(context.get());
});
// 父线程修改InheritableThreadLocal变量值
context.set("value2-set-in-parent");
fixedThreadPool.submit(()->System.out.println(context.get()));
}
3.TransmittableThreadLocal实现原理
根据上面示例debug代码你会发现,修饰线程池TtlExecutors.getTtlExecutorService()
底层也是通过修饰Runnable
实现的:
ini
private static ExecutorService fixedThreadPool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
TtlExecutors.getTtlExecutorService()
less
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
// 线程池封装增强
return new ExecutorServiceTtlWrapper(executorService, true);
}
来到ExecutorServiceTtlWrapper
类,你会发现它实现了ExecutorService
接口,实现了该接口相关的方法,同时这个类还有一个变量private final ExecutorService executorService;
: 这是典型的装饰器模式
scala
class ExecutorServiceTtlWrapper extends ExecutorTtlWrapper implements ExecutorService, TtlEnhanced {
private final ExecutorService executorService;
@NonNull
@Override
public Future<?> submit(@NonNull Runnable task) {
return executorService.submit(TtlRunnable.get(task, false, idempotent));
}
}
我这里给出它实现ExecutorService
接口的一个方法submit()
,可以看到最终还是通过TtlRunnable
修饰Runable
实现,所以核心逻辑就在TtlRunnable
TtlRunnable 核心代码:上面的TtlRunnable.get()
会调用```TtlRunnable``的构造方法
java
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
private final AtomicReference<Object> capturedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
// 捕获主线程的所有TransmittableThreadLocal和注册的ThreadLocal值。
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
@Override
public void run() {
// 获取主线程传递下来的TransmittableThreadLocal和ThreadLocal值,这已经在上面构造方法中捕获到
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
// 重放和备份
// 1.重放之前先备份子线程的ttl值,方便后续恢复
// 2.重放上面获取到主线程的ttl值,set到子线程中
final Object backup = replay(captured);
try {
// 执行子线程任务
runnable.run();
} finally {
// 恢复 恢复线程执行replay方法之前的TTL值
restore(backup);
}
}
//......
}
核心流程:
capture()
:创建任务时捕获当前线程的所有TTL变量replay()
:执行任务前,将捕获的TTL变量恢复到当前线程restore()
:执行完成后,恢复线程原有的TTL变量
这里关于这些核心点的源码就不展开说了,有兴趣的自行了解一波。
期望:上下文生命周期的操作从业务逻辑中分离出来。业务逻辑不涉及生命周期,就不会有业务代码如疏忽清理而引发的问题了。整个上下文的传递流程或说生命周期可以规范化成:捕捉、回放和恢复 这3个操作,即CRR(capture/replay/restore)
模式。更多讨论参见 Issue:能在详细讲解一下replay
、restore
的设计理念吗?#201
其实捕获主线程的TTL值,通过回放set到子线程中这比较好理解,但是这个备份和恢复就有点难明白了,因为在我们通常理解里面线程池的线程执行完任务之后,直接清除该线程的ttl值即可,因为线程已经结束了,备份线程原来的ttl值再恢复貌似没有什么意义~~~
其实有一种特殊情况:线程池满了且线程池使用的是CallerRunsPolicy
,则提交到线程池的任务在capture
捕获线程中直接执行,也就是直接在业务主线程中同步执行;如果你直接清除,则会对主线程的后续逻辑执行可能造成bug。
这是TTL使用的整体流程图:

4.TransmittableThreadLocal在日志收集记录系统上下文实战应用
spring boot项目中引入日志功能的starter:spring-boot-starter-logging
,底层使⽤了 logback + slf4j 组合作为默认底层⽇志。
之前我们总结过关于:Spring Boot项目如何实现分布式日志链路追踪。每个请求都使用一个唯一标识traceId
来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵),然后使用使用Logback的MDC
机制日志模板中加入traceId
标识,取值方式为%X{traceId}
。这样在收集的日志文件中就可以看到每行日志有一个traceId
值,每个请求的值都不一样,这样我们就可以根据traceId
查询过滤出一次请求的所有上下文日志了,格式如下:
xml
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="[%X{traceId}] [%-5p] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t@${PID}] %c %M : %m%n"/>
但是在多线程场景下traceId
就丢失了。要想解决这个问题就得借助今天主角TTL
4.2 logback
logback作为spring boot的默认日志,其实我们只需要重写logback对slf4j提供标准接口MDCAdapter
的实现即可,把logback的实现类LogbackMDCAdapter
代码抄一遍:
typescript
public class TtlLogbackMDCAdapter implements MDCAdapter {
private static final TtlLogbackMDCAdapter MDC_ADAPTER;
// BEWARE: Keys or values placed in a ThreadLocal should not be of a type/class
// not included in the JDK. See also https://jira.qos.ch/browse/LOGBACK-450
/**
* use com.alibaba.ttl.TransmittableThreadLocal
*/
final ThreadLocal<Map<String, String>> readWriteThreadLocalMap = new TransmittableThreadLocal<>();
final ThreadLocal<Map<String, String>> readOnlyThreadLocalMap = new TransmittableThreadLocal<>();
private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks();
static {
MDC_ADAPTER = new TtlLogbackMDCAdapter();
}
public static MDCAdapter getInstance() {
return MDC_ADAPTER;
}
/**
* Put a context value (the <code>val</code> parameter) as identified with the
* <code>key</code> parameter into the current thread's context map. Note that
* contrary to log4j, the <code>val</code> parameter can be null.
* <p/>
* <p/>
* If the current thread does not have a context map it is created as a side
* effect of this call.
* <p/>
* <p/>
* Each time a value is added, a new instance of the map is created. This is
* to be certain that the serialization process will operate on the updated
* map and not send a reference to the old map, thus not allowing the remote
* logback component to see the latest changes.
*
* @throws IllegalArgumentException in case the "key" parameter is null
*/
@Override
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
Map<String, String> current = readWriteThreadLocalMap.get();
if (current == null) {
current = new HashMap<>();
readWriteThreadLocalMap.set(current);
}
current.put(key, val);
nullifyReadOnlyThreadLocalMap();
}
/**
* Get the context identified by the <code>key</code> parameter.
* <p/>
* <p/>
* This method has no side effects.
*/
@Override
public String get(String key) {
Map<String, String> hashMap = readWriteThreadLocalMap.get();
if ((hashMap != null) && (key != null)) {
return hashMap.get(key);
} else {
return null;
}
}
/**
* <p>Remove the context identified by the <code>key</code> parameter.
* <p/>
*/
@Override
public void remove(String key) {
if (key == null) {
return;
}
Map<String, String> current = readWriteThreadLocalMap.get();
if (current != null) {
current.remove(key);
nullifyReadOnlyThreadLocalMap();
}
}
private void nullifyReadOnlyThreadLocalMap() {
readOnlyThreadLocalMap.set(null);
}
/**
* Clear all entries in the MDC.
*/
@Override
public void clear() {
readWriteThreadLocalMap.set(null);
nullifyReadOnlyThreadLocalMap();
}
/**
* <p>Get the current thread's MDC as a map. This method is intended to be used
* internally.</p>
*
* The returned map is unmodifiable (since version 1.3.2/1.4.2).
*/
public Map<String, String> getPropertyMap() {
Map<String, String> readOnlyMap = readOnlyThreadLocalMap.get();
if (readOnlyMap == null) {
Map<String, String> current = readWriteThreadLocalMap.get();
if (current != null) {
final Map<String, String> tempMap = new HashMap<>(current);
readOnlyMap = Collections.unmodifiableMap(tempMap);
readOnlyThreadLocalMap.set(readOnlyMap);
}
}
return readOnlyMap;
}
/**
* Return a copy of the current thread's context map. Returned value may be
* null.
*/
@Override
public Map<String, String> getCopyOfContextMap() {
Map<String, String> readOnlyMap = getPropertyMap();
if (readOnlyMap == null) {
return null;
} else {
return new HashMap<>(readOnlyMap);
}
}
/**
* Returns the keys in the MDC as a {@link Set}. The returned value can be
* null.
*/
public Set<String> getKeys() {
Map<String, String> readOnlyMap = getPropertyMap();
if (readOnlyMap != null) {
return readOnlyMap.keySet();
} else {
return null;
}
}
@Override
@SuppressWarnings("unchecked")
public void setContextMap(Map contextMap) {
if (contextMap != null) {
readWriteThreadLocalMap.set(new HashMap<String, String>(contextMap));
} else {
readWriteThreadLocalMap.set(null);
}
nullifyReadOnlyThreadLocalMap();
}
@Override
public void pushByKey(String key, String value) {
threadLocalMapOfDeques.pushByKey(key, value);
}
@Override
public String popByKey(String key) {
return threadLocalMapOfDeques.popByKey(key);
}
@Override
public Deque<String> getCopyOfDequeByKey(String key) {
return threadLocalMapOfDeques.getCopyOfDequeByKey(key);
}
@Override
public void clearDequeByKey(String key) {
threadLocalMapOfDeques.clearDequeByKey(key);
}
}
把原来的ThreadLocal
地方替换成TransmittableThreadLocal
即可。
通过spi机制加载重写的实现:
scala
public class TtlLogbackServiceProvider extends LogbackServiceProvider {
private MDCAdapter ttlMdcAdapter;
@Override
public void initialize() {
super.initialize();
this.ttlMdcAdapter = TtlLogbackMDCAdapter.getInstance();
((LoggerContext)super.getLoggerFactory()).setMDCAdapter(ttlMdcAdapter);
}
@Override
public MDCAdapter getMDCAdapter() {
return this.ttlMdcAdapter;
}
}
最后在项目resource
建一个目录/META-INF/services
,在目录下新建文件org.slf4j.spi.SLF4JServiceProvider
写入你刚刚重写实现类的全路径类名
c
com.plasticene.boot.web.core.log.logback.TtlLogbackServiceProvider
至此就能在多线程上下文场景中正常传递traceId
并被日志系统收集到了。
4.2 log4j2
log4j2是当下更主流的日志框架,其性能表现、功能特性都优与logback,一般建议新项目可以直接采用Log4j2,当然了这并不是说使用logback就不行了,其实也问题不大,要不然spring boot也不会用它做默认日志框架了。
要想用log4j2,就必须先剔除默认spring boot默认的logback,一般依赖配置如下:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
log4j2j集成TTL
实现多线程上下文传递功能更简单,只需要重写log4j2对ThreadContextMap
的默认实现DefaultThreadContextMap
,一样的套路把默认实现代码抄一遍:
typescript
public class TtlThreadContextMap implements ThreadContextMap {
private final ThreadLocal<Map<String, String>> localMap;
public TtlThreadContextMap() {
this.localMap = new TransmittableThreadLocal<>();
}
@Override
public void put(final String key, final String value) {
Map<String, String> map = localMap.get();
map = map == null ? new HashMap<>() : new HashMap<>(map);
map.put(key, value);
localMap.set(Collections.unmodifiableMap(map));
}
@Override
public String get(final String key) {
final Map<String, String> map = localMap.get();
return map == null ? null : map.get(key);
}
@Override
public void remove(final String key) {
final Map<String, String> map = localMap.get();
if (map != null) {
final Map<String, String> copy = new HashMap<>(map);
copy.remove(key);
localMap.set(Collections.unmodifiableMap(copy));
}
}
@Override
public void clear() {
localMap.remove();
}
@Override
public boolean containsKey(final String key) {
final Map<String, String> map = localMap.get();
return map != null && map.containsKey(key);
}
@Override
public Map<String, String> getCopy() {
final Map<String, String> map = localMap.get();
return map == null ? new HashMap<>() : new HashMap<>(map);
}
@Override
public Map<String, String> getImmutableMapOrNull() {
return localMap.get();
}
@Override
public boolean isEmpty() {
final Map<String, String> map = localMap.get();
return map == null || map.isEmpty();
}
@Override
public String toString() {
final Map<String, String> map = localMap.get();
return map == null ? "{}" : map.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
final Map<String, String> map = this.localMap.get();
result = prime * result + ((map == null) ? 0 : map.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof TtlThreadContextMap other)) {
return false;
}
final Map<String, String> map = this.localMap.get();
final Map<String, String> otherMap = other.getImmutableMapOrNull();
if (map == null) {
return otherMap == null;
} else {
return map.equals(otherMap);
}
}
}
把原来的ThreadLocal
地方替换成TransmittableThreadLocal
即可。
4.3 使用示例
代码如下:
java
private final ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
@Operation(summary = "测试traceId多线程上下文传递")
@GetMapping("/log/traceId")
public void testLogTraceId() {
// webTraceFilter会写入traceId
log.info("主线程traceId: {}", MDCTraceUtils.getTraceId());
executorService.submit(() -> {
log.info("子线程traceId: {}", MDCTraceUtils.getTraceId());
try {
// 睡眠2秒,等webTraceFilter清除traceId
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("睡眠结束子线程traceId: {}", MDCTraceUtils.getTraceId());
});
}
这里是在webTraceFilter
设置和清除一次请求的traceId
的,不了解套路的可以上面我们提到的之前总结的日志追踪文章链接跳转自行查看,这里的异步睡眠2s,等着webTraceFilter
清除了traceId,看对子线程是否有影响,并没有
ini
[1932435207898664960]-[INFO ]-[2025-06-10 21:50:35.020]-[http-nio-8882-exec-4]-[com.plasticene.boot.example.web.controller.TestController]-[testLogTraceId]:主线程traceId: 1932435207898664960
[1932435207898664960]-[INFO ]-[2025-06-10 21:50:35.037]-[pool-5-thread-1]-[com.plasticene.boot.example.web.controller.TestController]-[lambda$testLogTraceId$0]:子线程traceId: 1932435207898664960
[1932435207898664960]-[INFO ]-[2025-06-10 21:50:37.038]-[pool-5-thread-1]-[com.plasticene.boot.example.web.controller.TestController]-[lambda$testLogTraceId$0]:睡眠结束子线程traceId: 1932435207898664960