news 2026/10/9 5:38:20

ThreadLocal系列(四):父子线程信息传递与TTL

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ThreadLocal系列(四):父子线程信息传递与TTL

父子线程信息传递与TTL

前面介绍了ThreadLocal使用与内存泄漏防范,还从引用队列角度思考如何防范内存泄漏。
这篇文章是自己在实际中用到了RAG检索与回答用自定义线程池而不是tomcat线程池,防止tomcat线程池线程被占用导致无法处理其他请求。
其中用到了跨线程的上下文传递,就写个文章说一下。

父子线程传递ThreadLocal

问题

ThreadLocal做上下文传递,每个线程拿自己的用户信息,互不干扰,但如果子线程需要拿父线程的信息呢?

先做个测试

public class Problem { private static final ThreadLocal<String> USER = new ThreadLocal<>(); public static void main(String[] args) { USER.set("张三"); System.out.println("主线程用户" + USER.get()); new Thread(()->{ System.out.println("子线程用户" + USER.get()); }).start(); } }

结果
根据前面的内容,每个线程都有自己的ThreadLocal,所以可以理解下面的结果

主线程用户张三 子线程用户null

怎么解决这个问题?

InheritableThreadLocal

JDK提供了InheritableThreadLocal,子线程能从父线程复制一份

public class TestInheritable { private static final InheritableThreadLocal<String> USER = new InheritableThreadLocal<>(); public static void main(String[] args) { USER.set("张三"); System.out.println("主线程用户" + USER.get()); new Thread(()->{ System.out.println("子线程用户" + USER.get()); }).start(); } }

运行结果

主线程用户张三 子线程用户张三

拿到了,为什么可以拿到?

分析父子线程传递

其实从第一篇文章就有体现,在翻找第一篇文章的下面内容的时候,就找到了这个InheritableThreadLocal
第一篇的内容:

public class Thread implements Runnable { //省略。。。 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; //省略。。。 }

就在它的下方就有这个InheritableThreadLocal

它也是Thread内部的,进入Thread,有俩个init方法,看的是第二个

private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // 省略。。。 if (inheritThreadLocals && parent.inheritableThreadLocals != null) // 关键:parent的inheritableThreadLocals中复制过来 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; // 省略。。。 }

进入createInheritedMap

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }

再进去ThreadLocalMap

private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; // 拿到它爹的Entry数组 int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { //遍历这个数组 Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //这个key没被回收 if (key != null) { // key不为null,用childValue方法决定子线程的值 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }

从上面看到,return new ThreadLocalMap(parentMap)是把父线程的map放了进去进行遍历,
key没被回收的话就调用key.childValue方法,得到新的value再给Entry,Entry是ThreadLocalMap的内部类,用于存储ThreadLocal的key和value。

再进入childValue方法,不过不是进入上面这个childValue方法,而是进入InheritableThreadLocal的childValue方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } //省略。。。 }

这个方法是把传进来的e.value返回了回去
一些细节:

ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(),e.get()的e是Entry类型的。
前面第三个文章提到,ntry的get是构造方法的时候通过super构造的,而Entry又继承了WeakReference,而WeakReference构造方法又调用了super,WeakReference又继承了Reference,Reference里有个private T referent;
这个key层层super上去,其实就是这个referent。整个过程没有new,也就是说,ThreadLocal<Object> key = (ThreadLocal<Object>) e.get()中key和e.get()指的是堆里同一个对象。前面也提到,key指的是ThreadLocal对象
再往下看,Object value = key.childValue(e.value);而childValue在InheritableThreadLocal实现的是直接return了,和上面key一样也是复制引用,不复制对象
再往下看,Entry c = new Entry(key, value),它new了,也就是父的Entry和子的Entry不是指向同一个堆里的对象,但是Entry里的key和value都是复制引用而没有复制对象,所以Entry实际上也是浅拷贝。

整个流程:

父子线程信息传递的时候,父线程有个inheritableThreadLocals,子线程Thread在init的时候,拷贝父线程的ThreadLocalMap信息(浅拷贝),这样就获取了父线程的ThreadLocal的Entry,每个Entry都有key和value

InheritableThreadLocal的问题

从上面看出,它获取父线程的值的时候是在自己这个线程创建的时候调用init方法,也就是创建时复制,提交时不复制
意思就是线程池里复用的线程,想拿父线程的信息只能在初始化的时候拿一次,之后父线程ThreadLocalMap里新增了其他Entry,子线程它拿不到了

public class InheritableThreadLocalProblem { // 说明InheritableThreadLocal的问题 private static final InheritableThreadLocal<String> USER = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 用线程池做线程复用 ExecutorService pool = Executors.newFixedThreadPool(2); // 主线程set值 USER.set("张三"); //提交到线程池,线程池创建核心线程,创建线程会获取父线程信息 pool.submit(()->{ System.out.println("线程池用户为" + USER.get()); }); //主线程更新 USER.set("李四"); pool.submit(()->{ System.out.println("线程池用户为" + USER.get()); }); //线程池执行任务是异步的,防止main直接没了导致线程池任务没跑就关掉 Thread.sleep(1000); pool.shutdown(); } }

执行结果

线程池用户为张三 线程池用户为李四

输出是正确的,因为核心线程弄了俩,set之后pool里调用的时候创建了新线程,没有复用。
多执行几次,这次三次set,同时把线程池哪个线程执行的也打印出来。

public class InheritableThreadLocalProblem { // 说明InheritableThreadLocal的问题 private static final InheritableThreadLocal<String> USER = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 用线程池做线程复用 ExecutorService pool = Executors.newFixedThreadPool(2); // 主线程set值 USER.set("张三"); //提交到线程池,线程池创建核心线程,创建线程会获取父线程信息 pool.submit(()->{ System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); //主线程更新 USER.set("李四"); pool.submit(()->{ System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); USER.set("王五"); pool.submit(()->{ System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); //线程池执行任务是异步的,防止main直接没了导致线程池任务没跑就关掉 Thread.sleep(1000); pool.shutdown(); } }

执行结果符合预期,主线程更新了用户信息,线程池却并没有进行更新。

pool-1-thread-1 用户:张三 pool-1-thread-2 用户:李四 pool-1-thread-1 用户:张三

解决方案

为什么解决

场景:Tomcat线程池复用,核心线程执行任务的时候调用线程池执行耗时长的任务,而线程池又需要Tomcat当前线程的上下文信息,这就需要上下文信息支持跨线程传递

TransmittableThreadLocal(TTL)

TransmittableThreadLocal,专门解决线程池场景的传递问题
先导入依赖

<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.5</version> </dependency>

测试

public class TTLTest { private static final ThreadLocal<String> USER = new TransmittableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 用线程池做线程复用 ExecutorService pool = Executors.newFixedThreadPool(2); //装饰线程池,用ttl执行任务 ExecutorService ttlPool = TtlExecutors.getTtlExecutorService(pool); // 主线程set值 USER.set("张三"); //提交到线程池 ttlPool.submit(() -> { System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); //主线程更新 USER.set("李四"); ttlPool.submit(() -> { System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); USER.set("王五"); ttlPool.submit(() -> { System.out.println(Thread.currentThread().getName() + " 用户:" + USER.get()); }); //线程池执行任务是异步的,防止main直接没了导致线程池任务没跑就关掉 Thread.sleep(1000); pool.shutdown(); } }

结果

pool-1-thread-1 用户:张三 pool-1-thread-2 用户:李四 pool-1-thread-1 用户:王五
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 5:34:51

GitHub热榜解读:从AI Agent到本地优先,如何看趋势选项目

GitHub热榜的日榜&#xff08;2026-10-02&#xff09;出来了。我从几年前开始养成了每天早晨刷一遍热榜的习惯&#xff0c;不为别的&#xff0c;就想看看社区里最近在折腾什么。今天这份榜单挺有意思&#xff0c;AI Agent类的项目依然强势&#xff0c;但中间混进去好几个做本地…

作者头像 李华
网站建设 2026/10/9 5:32:44

VS Code接入Claude第三方API:Base URL、API Key与settings.json配置指南

1. 先理清思路&#xff1a;扩展、API 与模型服务之间到底怎么配合这两年用 VS Code 做 AI 辅助开发&#xff0c;几乎成了不少开发者的日常。尤其是 Claude 这类模型能力强、代码理解度高的助手&#xff0c;集成进编辑器之后&#xff0c;写代码、改 bug、补测试的效率提升非常明…

作者头像 李华