Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
huhaosumail opened this issue Jun 19, 2023 · 3 comments
Closed

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

huhaosumail opened this issue Jun 19, 2023 · 3 comments
Assignees
Labels
📐 design discussion 🔰 first nice issue 👍 😖 no runnable reproducible demo 😵 please provide a simple runnable demo that reproduce the problem ❓question Further information is requested

Comments

@huhaosumail
Copy link

huhaosumail commented Jun 19, 2023

我想问下,下面这段逻辑,为什么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 oldratlee added the ❓question Further information is requested label Jun 19, 2023
@oldratlee oldratlee self-assigned this Jun 19, 2023
@oldratlee
Copy link
Member

oldratlee commented Jun 19, 2023

关于「不修饰」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

  • 已有的讨论: disable Inheritable when it's not necessary and buggy(eg. has potential memory leaking problem) #100
  • 对应TTL已实现的功能,即上面举例的DisableInheritableThreadFactoryWrapper
    * Wrapper of {@link ThreadFactory}, disable inheritable.
    *
    * @param threadFactory input thread factory
    * @see DisableInheritableThreadFactory
    * @see TtlForkJoinPoolHelper#getDisableInheritableForkJoinWorkerThreadFactory
    * @since 2.10.0
    */
    @Nullable
    @Contract(value = "null -> null; !null -> !null", pure = true)
    public static ThreadFactory getDisableInheritableThreadFactory(@Nullable ThreadFactory threadFactory) {
    if (threadFactory == null || isDisableInheritableThreadFactory(threadFactory)) return threadFactory;
    return new DisableInheritableThreadFactoryWrapper(threadFactory);
    }
    /**
    * Wrapper of {@link Executors#defaultThreadFactory()}, disable inheritable.
    *
    * @see #getDisableInheritableThreadFactory(ThreadFactory)
    * @see TtlForkJoinPoolHelper#getDefaultDisableInheritableForkJoinWorkerThreadFactory()
    * @since 2.10.0
    */
    @NonNull
    public static ThreadFactory getDefaultDisableInheritableThreadFactory() {
    return getDisableInheritableThreadFactory(Executors.defaultThreadFactory());

更多说明参见已有 issue

@oldratlee oldratlee reopened this Jun 19, 2023
@oldratlee oldratlee added the 😖 no runnable reproducible demo 😵 please provide a simple runnable demo that reproduce the problem label Jun 19, 2023
@huhaosumail
Copy link
Author

huhaosumail commented Jun 20, 2023

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

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

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

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

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

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

  • TtlExecutors.getDefaultDisableInheritableThreadFactory()
  • 或者
    TransmittableThreadLocal<String> t1 = new TransmittableThreadLocal<String>() {
        protected String childValue(String parentValue) {
            return initialValue();
        }
    }

声明解决泄漏问题。

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

@oldratlee
Copy link
Member

oldratlee commented Jun 20, 2023

@huhaosumail COOOL 👍 🚀

@oldratlee oldratlee changed the title 关于不修饰runnable使用ttl导致子线程泄露问题 关于不修饰runnable使用TTL导致子线程泄露问题 Dec 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📐 design discussion 🔰 first nice issue 👍 😖 no runnable reproducible demo 😵 please provide a simple runnable demo that reproduce the problem ❓question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants