TransmittableThreadLocal剖析

1. ThreadLocal系列介绍

1.1. 什么是TransmittableThreadLocal

TransmittableThreadLocal 是 Alibaba 开源的 Java 库 Transmittable Thread Local (TTL) 中的一个类。它扩展了 Java 标准库中的 java.lang.ThreadLocal 类,提供了跨线程传递本地变量(如跨线程池提交任务时)的能力。这在处理需要使用线程池的异步编程和并发场景下有大用处。

要关注的是,TransmittableThreadLocal 需要配合 TtlExecutors 装饰器使用,通过 TtlExecutors 装饰器包装的线程池可以确保在原始线程和执行任务的线程之间共享 TransmittableThreadLocal

 

ini

代码解读

复制代码

// 创建一个ExecutorService线程池 ExecutorService pool = Executors.newFixedThreadPool(1); // 运用TtlExecutors装饰器来装饰线程池 ExecutorService ttlPool = TtlExecutors.getTtlExecutorService(pool);

1.2. ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal区别

  1. ThreadLocal:父线程不会传递threadLocal副本到子线程中;
  2. InheritableThreadLocal:在子线程创建的时候,父线程会把threadLocal拷贝到子线程中,但是线程池的线程如果为核心线程不会每次都重新创建,所以这时核心线程还是拿到首次创建时拷贝的值;
  3. TransmittableThreadLocal:每次线程执行任务前都会将父线程在threadLocal中最新的值拷贝到子线程中,解决了InheritableThreadLocal线程池无法传递本地副本的问题。

2. 代码分析

2.1. ThreadLocal案例

 

ini

代码解读

复制代码

@Test public void threadLocalTest() { ThreadLocal<String> local = new ThreadLocal<>(); try { local.set("我是主线程"); ExecutorService executorService = Executors.newFixedThreadPool(1); CountDownLatch c1 = new CountDownLatch(1); CountDownLatch c2 = new CountDownLatch(1); executorService.execute(() -> { System.out.println("线程1结果:" + local.get()); c1.countDown(); }); c1.await(); executorService.execute(() -> { System.out.println("线程2结果:" + local.get()); c2.countDown(); }); c2.await(); executorService.shutdownNow(); } catch (InterruptedException e) { e.printStackTrace(); } finally { //使用完毕,清除线程中ThreadLocalMap中的key。 local.remove(); } }

运行结果:

通过结果我们可以看出,只使用ThreadLocal,线程池中的子线程是无法拿到主线程设置到ThreadLocal的值的。

2.2. InheritableThreadLocal案例

 

csharp

代码解读

复制代码

@Test public void inheritableThreadLocalTest() throws InterruptedException { ThreadLocal<String> local = new InheritableThreadLocal<>(); local.set("我是主线程"); new Thread(() -> { System.out.println("子线程1结果:" + local.get()); }).start(); }

运行结果:

固定一个core线程的线程池再试:

 

csharp

代码解读

复制代码

@Test public void inheritableThreadLocalTest() throws InterruptedException { ThreadLocal<String> local = new InheritableThreadLocal<>(); try { local.set("我是主线程"); ExecutorService executorService = Executors.newFixedThreadPool(1); CountDownLatch c1 = new CountDownLatch(1); CountDownLatch c2 = new CountDownLatch(1); //初始化init的时候,赋予了父线程的ThreadLocal的值 executorService.execute(() -> { System.out.println("线程1结果:" + local.get()); c1.countDown(); }); c1.await(); //主线程修改值 local.set("修改主线程"); //再次调用,查看效果 executorService.execute(() -> { System.out.println("线程2结果:" + local.get()); c2.countDown(); }); c2.await(); executorService.shutdownNow(); } catch (InterruptedException e) { e.printStackTrace(); } finally { //使用完毕,清除线程中ThreadLocalMap中的key。 local.remove(); } }

运行结果:

通过实验我们可以看出,当把核心线程数设置为1时,主线程中ThreadLocal值再更改,子线程获取到的仍然是首次赋值拿到的值,故其弊端是核心线程不会每次重建,所以值也就不会更新。

2.3. TransmittableThreadLocal案例

 

ini

代码解读

复制代码

@Test public void transmittableThreadLocalTest() throws InterruptedException { TransmittableThreadLocal<String> local = new TransmittableThreadLocal<>(); local.set("我是主线程"); //生成额外的代理 ExecutorService executorService = Executors.newFixedThreadPool(1); //核心装饰代码 executorService = TtlExecutors.getTtlExecutorService(executorService); CountDownLatch c1 = new CountDownLatch(1); CountDownLatch c2 = new CountDownLatch(1); executorService.execute(() -> { System.out.println("我是线程1结果:" + local.get()); c1.countDown(); }); c1.await(); local.set("修改主线程"); System.out.println("此时主线程值:" + local.get()); executorService.execute(() -> { System.out.println("我是线程2结果:" + local.get()); c2.countDown(); }); c2.await(); }

运行结果:

通过上面的实验可知,使用TransmittableThreadLocal存储配合TtlExecutors,线程池中的子线程每次都能拿到主线程最近一次在TransmittableThreadLocal存的值。

3. 原理分析

3.1. InheritableThreadLocal

首先来看,为什么案例 2.2 中的InheritableThreadLocal只能拿到在创建线程前最近一次父线程设置的值呢?原因如下:

3.1.1. 父线程set

父线程往InheritableThreadLocal设置值时,在创建ThreadLocalMap时会最后赋值给InheritableThreadLocal的inheritableThreadLocals变量(也就是说父线程往InheritableThreadLocal设置的值都保存在inheritableThreadLocals变量)。

线程池当第一个任务进来时会去创建Worker,在Worker构造函数中会通过线程工厂创建线程。

这里的parent.inheritableThreadLocals就是父线程InheritableThreadLocal设置的值,可以看到在子线程创建时拷贝给了子线程的inheritableThreadLocals变量(也可以理解为这里子线程继承了父线程的inheritableThreadLocals变量,但是只有在创建子线程前会做)。

3.1.2. 子线程get

可以看到子线程get的时候获取值就是从inheritableThreadLocals中去取,拿到的也就是创建子线程前父线程拷贝过来的inheritableThreadLocals

在执行了local.set("修改主线程");后,确实修改了主线程的内容。

但到了子线程中,其值依然是之前的“我是主线程”,由于设定的线程池核心数为1,这个线程又处于空闲状态,所以线程池不会再次创建线程,而是复用已第一次创建的线程。可以看到拿到的inheritableThreadLocals还是创建线程时父线程拷贝过来的值。这就能解释2.2案例中返回的结果了。

3.2. TransmittableThreadLocal

3.2.1. holder

在TransmittableThreadLocal中会维护一个holder变量,该变量保存当前线程所有的TransmittableThreadLocal。

在执行set或get方法时,父线程会将存放在TransmittableThreadLocal的数据都放在holder变量中。

在交给TtlExecutors线程池执行任务前会创建TtlRunnable(excute() -> TtlRunnable -> capture() -> run() ->replay() ),TtlRunnable会在创建时初始化执行capture() 方法,可以看到 capture 的逻辑其实就是返回一个快照,而这个快照就是遍历 holder 获取所有存储在 holder 里面的 TTL ,返回一个新的 map。

3.2.2. replay

在具体执行任务时TtlRunnable执行的run方法主要分为四部:

  1. 拿到父类本地变量拷贝
  2. 赋值给当前线程(线程池内的某线程),并保存之前的本地变量
  3. 执行任务逻辑
  4. 复原当前线程之前的本地变量

再来看下replay,即如何将父类的本地变量赋值给当前线程的,主要逻辑就是先备份当前线程目前存储的值(以备后续恢复),然后拷贝父线程中的值进行覆盖,最后返回备份。

其中实现此逻辑的重点就在于capturedbackup。captured 和 backup 都是 HashMap<TransmittableThreadLocal, Object> 对象,但它们在逻辑上扮演着不同的角色,具有不同的用途。

3.2.2.1. captured

captured 是传递给 replay 方法的参数。它包含的是从其他线程捕获的 TransmittableThreadLocal 的值。换句话说,captured 是线程本地变量(TTL)的快照,它保存的是在某个特定时间点,这些变量在其他线程中的状态。这些值将被“重播”到当前线程中。

3.2.2.2. backup

backup 是在 replay 方法中创建的一个新的 HashMap<TransmittableThreadLocal<Object>, Object> 对象。它的主要作用是备份当前线程中的 TransmittableThreadLocal 值。这是为了在重播 captured 的值之前,保存当前线程中的 TTL 值,以便能在需要时恢复这些值。

总结一下就是,

  • captured 保存的是从其他线程捕获的 TTL 值,准备重播到当前线程中。
  • backup 保存的是当前线程在重播前的 TTL 值,目的是为了在重播后能恢复这些值。

通过这样设计,可以确保重播在其他线程捕获的 TTL 值之前和之后,都能保证当前线程的已有状态不被永久改变。

可以看下父线程如何将本地变量拷贝到当前线程的,其实就是 for 循环进行 set ,从这里也可以得知为什么上面需要移除父线程没有的 TTL,因为这里只是进行了 set,如果不 remove 当前线程的本地变量,那就不是完全继承自父线程的本地变量了,可能掺杂着之前的本地变量,也就是不干净了,防止这种干扰。

最后恢复操作,主要是去除子线程里backup没有的TransmittableThreadLocal给移除,恢复上下文。

4. 内存泄漏

提到ThreadLocal,难免就会想到不正确使用带来内存泄漏问题,接下来让我们一起来分析下。

4.1. 案例一

4.1.1. 代码

 

dart

代码解读

复制代码

@Test public void leakMapTest() throws InterruptedException { TransmittableThreadLocal<Map<String, String>> t1 = new TransmittableThreadLocal<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 3000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100)); try { HashMap<String, String> map = new HashMap<>(); map.put("key1", "value1"); t1.set(map); threadPoolExecutor.execute(() -> { System.out.println("t1: " + Thread.currentThread().getName() + ":" + t1.get()); Map<String, String> stringMap = t1.get(); stringMap.put("key2", "value2"); System.out.println("t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); t1.remove(); } finally { Thread.sleep(2000); System.out.println("主线程试试 t1: " + Thread.currentThread().getName() + ":" + t1.get()); // 为什么这里能拿到值呢 threadPoolExecutor.execute(() -> { System.out.println("异步线程试试 t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); } }

运行结果:

如果TransmittableThreadLocal不配合TtlExecutors使用,而是使用普通线程池,只要线程池内的线程不被销毁,该线程就会一直持有TransmittableThreadLocal中设定的值,这部分内容就一直不会被GC。

4.1.2. 分析

为何会造成泄漏?我们需要在线程池创建线程的地方去看下

可以看到在创建线程时断点会走到ThreadLocal.createInheritedMap,断点走到这步的具体原因在于父线程中使用了TransmittableThreadLocal,而TransmittableThreadLocal继承了InheritableThreadLocal,而父线程往TransmittableThreadLocal设置value时会创建InheritableThreadLocal创建ThreadLocalMap并赋值给inheritableThreadLocals,所以可以看到parent.inheritableThreadLocals是有值的。

在ThreadLocalMap中会将父线程inheritableThreadLocals深拷贝一份,然后给子线程的inheritableThreadLocals

这样也就能理解了,外部父线程就算对TransmittableThreadLocal做了清理,清理的是父线程inheritableThreadLocals的值,子线程inheritableThreadLocals引用的值只要线程不会销毁就会一直持有,也不会被GC掉。

4.1.3. 结论

使用TransmittableThreadLocal需要线程池配合使用装饰器,即TtlExecutors,否则会有泄漏风险。

关于不修饰runnable使用TTL导致子线程泄露问题

4.2. 案例二

4.2.1. 代码

 

dart

代码解读

复制代码

@Test public void leakMapTest2() throws InterruptedException { TransmittableThreadLocal<Map<String, String>> t1 = new TransmittableThreadLocal<Map<String, String>>() { @Override protected Map<String, String> initialValue() { //线程池以及父子线程之间的值是浅拷贝即引用关系,所以使用线程安全的hashmap return new ConcurrentHashMap<>(8); } }; ExecutorService executorService = Executors.newFixedThreadPool(1); executorService = TtlExecutors.getTtlExecutorService(executorService); try { HashMap<String, String> map = new HashMap<>(); map.put("key1", "value1"); t1.set(map); executorService.execute(() -> { Map<String, String> stringMap = t1.get(); stringMap.put("key2", "value2"); t1.set(stringMap); System.out.println("t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); t1.remove(); } finally { Thread.sleep(2000); System.out.println("主线程试试t1: " + Thread.currentThread().getName() + ":" + t1.get()); executorService.execute(() -> { System.out.println("异步线程试试t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); } }

运行结果:

4.2.2. 分析

从结果上看父线程对TransmittableThreadLocal进行remove后,子线程再次执行从TransmittableThreadLocal是获取不到值了,看起来value都被回收了。但是当我们对finally模块的子线程执行execute进行debug发现子线程还是引用到了Map,而这个Map里的值就是父线程回收前的值。

debug打进去发现子线程执行前会将父线程TransmittableThreadLocal值拷贝过来,这里captured没有值,是正常的。

意想不到的事情发生了,从上图可以看出holder中的TransmittableThreadLocal还是会间接对map持有引用,map中的值也是这个子线程创建的时候从当时的父线程拷贝过来的,也可理解为子线程的inheritableThreadLocals一直对该map持有引用。父线程中的TransmittableThreadLocal.remove其实只是让父线程移除了指向这个map的引用。也就是说子线程继承了主线程,想要把这部分value得以销毁,必须要把该线程也要销毁了。

这样带来的问题是如果每次用这个线程对这个map进行add,那就回越来越多,都得不到回收了。

结论2:要禁用继承本地线程,否则GC依赖于线程池的销毁。

4.3. 案例三(解决4.2中内存泄漏)

有两种方式可以解:

  • TtlExecutors.getDefaultDisableInheritableThreadFactory()业务操作前 清空线程池线程上下文;
  • 或者声明解决泄漏问题,父子线程继承的时候 子线程初始化为空值。如下代码:
 

typescript

代码解读

复制代码

TransmittableThreadLocal<String> t1 = new TransmittableThreadLocal<String>() { protected String childValue(String parentValue) { return initialValue(); } }

例采用第二种方式:

 

typescript

代码解读

复制代码

@Test public void leakMapTest2() throws InterruptedException { TransmittableThreadLocal<Map<String, String>> t1 = new TransmittableThreadLocal<Map<String, String>>() { @Override protected Map<String, String> initialValue() { //线程池以及父子线程之间的值是浅拷贝即引用关系,所以使用线程安全的hashmap return new ConcurrentHashMap<>(8); } @Override protected Map<String, String> childValue(Map<String, String> parentValue) { return initialValue(); } }; ExecutorService executorService = Executors.newFixedThreadPool(1); executorService = TtlExecutors.getTtlExecutorService(executorService); try { HashMap<String, String> map = new HashMap<>(); map.put("key1", "value1"); t1.set(map); executorService.execute(() -> { Map<String, String> stringMap = t1.get(); stringMap.put("key2", "value2"); t1.set(stringMap); System.out.println("t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); t1.remove(); } finally { Thread.sleep(2000); System.out.println("主线程试试t1: " + Thread.currentThread().getName() + ":" + t1.get()); executorService.execute(() -> { System.out.println("异步线程试试t1: " + Thread.currentThread().getName() + ":" + t1.get()); }); } }

运行结果:

让TransmittableThreadLocal实现childValue,让子线程初始化时指向空集合,这样子线程holder指向的map永远都是空集合,完全依靠子线程执行任务前拷贝主线程的值,执行完再还原为空集合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值