alibaba / transmittable-thread-local

📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.
https://github.com/alibaba/transmittable-thread-local
Apache License 2.0
7.59k stars 1.69k forks source link

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

Closed huhaosumail closed 1 year ago

huhaosumail commented 1 year ago

我想问下,下面这段逻辑,为什么finally的异步逻辑还是能获取到TTL的值:

private static TransmittableThreadLocal t1  = new TransmittableThreadLocal();
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

@GetMapping("/t1")
public void t1() throws InterruptedException {
    try{
        System.out.println("t1 请求");
        t1.set("val1");

        threadPoolExecutor.execute(()->{
            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());
        });
    }
}
oldratlee commented 1 year ago

关于「不修饰」runnable使用ttl导致子线「泄露」

首先请以正确的方式使用TTL;参见项目文档 User Guide。

如果不做相应的修饰,TTL会退化成InheritableThreadLocal, 出现你说的情况是预期的(多提交几次可能非必现,与提交时间点/线程池配置相关); 更多请了解调研InheritableThreadLocalThreadPoolExecutor。 @huhaosumail

其次请提供一个 极简的 可运行复现问题 的代码实现/Demo。推荐提供成一个单独的工程(Github repo),或是 一个运行出的问题的UT(可以Fork TTL加一个UT)。这样可以:

一些相关说明

传递并不会消耗父线程的上下文。 @huhaosumail

要不只有一个子线程能拿到上下文了。 (这样的功能,在日常业务中,是奇怪的、应该也无用)

如果这导致业务的内存泄露,可以自己在finallyremove一下。

更多TTL的了解资料可以看看:

另一个相关「泄露」的使用注意事项。 @huhaosumail

JDK ThreadLocal也有「内存泄露」的使用注意事项 💕)

请使用getDisableInheritableThreadFactory(...) wrapper。

解决方法是

  • 线程池线程不应该有无用的上下文,
  • 或说 保证线程池线程刚开始时(所有业务逻辑的外层) 应该是 空的上下文,做好清空操作。

TTL有讨论 & 提供了相应的功能实现: @OrientationZero

更多说明参见已有 issue

huhaosumail commented 1 year ago

感谢大佬,上面的Case自己也整理清楚了,说明如下:

这里的最主要是识别TransmittableThreadLocal继承自JDKInheritableThreadLocal,参考上面的代码当t1.set("val1"),执行后相当于主线程InheritableThreadLocal这个线程绑定的值有值了。

之后第一次ThreadPoolExecutor执行异步任务的时候,会初始化线程,初始化线程的特性会子线程也继承父线程;也就是主线程的InheritableThreadLocal

这个时候相当于父子线程都携带了InheritableThreadLocal,后续主线程remove操作,remove的是主线程的InheritableThreadLocalfinally执行第二次异步任务的时候,线程池的线程是携带InheritableThreadLocal,所以导致能够取到值。

即便使用TtlRunnable.get(runnable)修饰第二个任务,能够符合预期获取空值,因为修饰后获取到的是主线程已经removeInheritableThreadLocal

但是会发生泄漏的问题,并没有remove掉子线程的InheritableThreadLocal,需要使用

声明解决泄漏问题。

第一种是业务操作前 清空线程池线程上下文; 第二种是父子线程继承的时候 子线程初始化为空值。

oldratlee commented 1 year ago

@huhaosumail COOOL 👍 🚀