TransmittableThreadLocal内存泄漏问题排查解决

背景

线上使用TransmittableThreadLocal解决线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。运行一段时间后,出现内存泄漏,进程被杀。后续回滚。进行分析。

使用知识储备

关于TransmittableThreadLocal的源码分析和使用请参考

TTL的整理总结

问题示例

•我们先看一段使用TransmittableThreadLocal示例代码

    /**
     * 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

    @GetMapping("/ttlLeak")
    public void ttlLeak() throws InterruptedException {
        try{
            transmittableThreadLocal.set("val");
            Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable1));
        }finally {
            //删除上下文
            transmittableThreadLocal.remove();
            Thread.sleep(2000);
            System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());

            Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(runnable2);
        }
    }

运行后得到如下结果:

通过上图可以看到第一次执行异步任务的时候能够获取到值,之后主线程进行remove操作,主线程获取不到为空,第二个异步任务执行的时候能够获取到值。这种情况下能够说明remove操作对主线程起效果,但是对子线程没有效果。子线程也有父线程也就是主线程的上下文信息。并且remove不掉。子线程是不是存在泄漏呢?那么有人说你没有规范使用TransmittableThreadLocal,接下来看下一段代码

  private static TransmittableThreadLocal transmittableThreadLocal  = new TransmittableThreadLocal();

    /**
     * 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

    @GetMapping("/ttlLeak")
    public void ttlLeak() throws InterruptedException {
        try{
            transmittableThreadLocal.set("val");
            Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable1));
        }finally {
            //删除上下文
            transmittableThreadLocal.remove();
            Thread.sleep(2000);
            System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());

            Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable2));
        }
    }

和上一段代码的区别是最后一个异步任务使用了TtlRunnable来包装,我们看下效果如下:

 很明显前面输出一致,最后一个没有获取到值了,那么是不是在TtlRunnable.get(runnable2)进行remove了呢?把当前子线程的上下文信息给remove了呢?我们再看一段代码如下:

 private static TransmittableThreadLocal transmittableThreadLocal  = new TransmittableThreadLocal();

    /**
     * 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

    @GetMapping("/ttlLeak")
    public void ttlLeak() throws InterruptedException {
        try{
            transmittableThreadLocal.set("val");
            Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable1));
        }finally {
            //删除上下文
            transmittableThreadLocal.remove();
            Thread.sleep(2000);
            System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());

            Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable2));

            Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(runnable3);
        }
    }

添加了一段异步任务3的逻辑,我们看下效果如下:

 可以看到第三个异步任务仍然可以拿到值,到这个时候我们能够确认TtlRunnable.get(runnable2)

并不能清除上下文信息。会存在泄漏。

泄漏原因定位

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

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

这个时候相当于父子线程都携带了InheritableThreadLocal,后续主线程remove操作,remove的是主线程的InheritableThreadLocalfinally执行第二次异步任务的时候,

获取值为空是因为异步任务用TtlRunnable修饰,用它修饰的话,执行任务前会将用父线程也就是主线程的上下文信息给子线程,这个时候主线程已经被清空,所以展示为空。但是执行完任务后会将子线程原来的上下文信息覆盖给当前子线程。也就是归还回去子线程原来的上下文信息。参考源码如下:

 所以最后一个异步任务执行的时候会拿到值 因为上下文信息依旧存在。

线程池的线程是携带InheritableThreadLocal,所以导致能够取到值。

这就是内存泄漏原因,看起来是TransmittableThreadLocal的泄漏其实是JDK的InheritableThreadLocal的泄漏。在父子线程传递的过程中,会让子线程也携带父线程的InheritableThreadLocal的上下文信息。



解决方案

第一种是业务操作前 清空线程池线程上下文;

 private static TransmittableThreadLocal transmittableThreadLocal  = new TransmittableThreadLocal();

    /**
     * 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100),TtlExecutors.getDefaultDisableInheritableThreadFactory());

    @GetMapping("/ttlLeak")
    public void ttlLeak() throws InterruptedException {
        try{
            transmittableThreadLocal.set("val");
            Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable1));
        }finally {
            //删除上下文
            transmittableThreadLocal.remove();
            Thread.sleep(2000);
            System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());

            Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable2));

            Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(runnable3);
        }
    }

效果如下:

可以看到最后一个异步任务也拿不到值了,说明异步线程的上下文也被清空了。 

实现原理参考TransmittableThreadLocal提供了声明线程池初始化前都清空上下文信息,源码如下:

第二种是父子线程继承的时候 子线程初始化为空值。

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

    /**
     * 声明一个最大线程数和核心线程数都为1的线程池 都为1 只是为了方便测试 防止不同线程混乱结果
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

    @GetMapping("/ttlLeak")
    public void ttlLeak() throws InterruptedException {
        try{
            transmittableThreadLocal.set("val");
            Runnable runnable1 = () -> System.out.println("异步任务1: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable1));
        }finally {
            //删除上下文
            transmittableThreadLocal.remove();
            Thread.sleep(2000);
            System.out.println("主线程: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());

            Runnable runnable2 = () -> System.out.println("异步任务2: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(TtlRunnable.get(runnable2));

            Runnable runnable3 = () -> System.out.println("异步任务3: " +Thread.currentThread().getName()+":"+ transmittableThreadLocal.get());
            executor.execute(runnable3);
        }
    }



可以看到最后一个异步任务也拿不到值了,说明异步线程的上下文也被清空了。 

原理在于声明了传递过程中子线程初始化的时候值为空,无视parentValue并不继承父线程的值。

与原作者的讨论分析过程

感谢鼎哥的帮助

关于不修饰runnable使用ttl导致子线程泄露问题 · Issue #521 · alibaba/transmittable-thread-local · GitHub

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内存泄漏是指在程序执行过程中,动态分配的内存没有被正确释放,导致内存空间被占用并无法再被重复利用的问题内存泄漏问题排查是找出造成内存泄漏的代码和地方。 在C++中,我们可以通过重载malloc函数来帮助排查内存泄漏问题。malloc函数用于分配内存空间,我们可以自定义一个重载函数,在该函数中添加一些记录和追踪内存分配的操作。 我们可以重载malloc函数来统计分配的内存大小和数量,并将分配的内存记录到一个容器中。可以使用一个全局的哈希表或者链表,每次调用重载的malloc函数时,记录下分配的内存大小和指针地址。当程序结束时,可以输出这个容器中的信息,包括每个分配的内存的大小和对应的指针地址。 通过重载malloc函数,我们可以很容易地定位到哪些地方分配内存后没有及时释放。我们可以在程序中的一些重要位置,比如函数或循环的入口和出口,输出当前的内存分配信息。通过对比入口和出口处的内存分配信息,可以找出内存泄漏的位置。 当然,重载malloc函数仅仅是一种辅助手段,它依赖于程序员的使用和分析。在使用malloc函数时,程序员需要有意识地进行内存的释放操作。只有当程序员规范使用malloc函数,并且及时释放内存时,才能避免内存泄漏问题的发生。 总之,通过重载malloc函数,我们可以在程序运行过程中动态地追踪内存分配的情况,并辅助排查内存泄漏问题。但是,重载malloc函数并不能完全解决内存泄漏问题,程序员仍然需要注意内存的释放操作,以避免内存泄漏的发生。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值