父子线程信息传递与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 用户:王五