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

TransmittableThreadLocal.Transmitter#restore 方法是否会导致内存泄漏? #321

Closed
StrongBanana opened this issue Oct 18, 2021 · 7 comments
Assignees
Labels

Comments

@StrongBanana
Copy link

StrongBanana commented Oct 18, 2021

restore方法最终会调用到ThreadLocal.set(),之后没有主动调用remove;是否会因此产生内存泄漏?

@StrongBanana
Copy link
Author

StrongBanana commented Oct 18, 2021

image

如图,restoreTtlValues方法remove的是当前线程基于backup新增的变量,对于backup的变量会和当前线程进行绑定(setTtlValuesTo);

那么在执行完setTtlValuesTo后,如果当前线程和调用线程并不是同一线程
(因为使用CallerRunsPolicy拒绝策略或ForkJoinPool导致调用线程和被调用线程数同一个)
的情况下,那么会发生内存泄漏吗?

@oldratlee oldratlee added the ❓question Further information is requested label Oct 20, 2021
@oldratlee
Copy link
Member

oldratlee commented Oct 20, 2021

简单的回答:Transmitter#restore方法 不会导致内存泄漏。 @StrongBanana


内存泄漏问题 在具体场景时可以构造对应的Case有效地测试到;
你可以直接测试一下,欢迎反馈你的测试结果~ @StrongBanana

当然测试 不能替代 基于代码逻辑的分析。

基于的代码分析你继续完成,以形成结论? @StrongBanana 💕 🙏

针对你这个问题,分析代码 可能是一个最可信的方式。
分析理清楚有结论之后,表述出来变成 文字或文章,可能还要花(不少)时间~ 😸
(尤其是在涉及并发时)


如果分析过程有困难,可以看看、参考 下面的内容: @StrongBanana

@oldratlee
Copy link
Member

oldratlee commented Oct 24, 2021

如图,restoreTtlValues方法remove的是当前线程基于backup新增的变量,对于backup的变量会和当前线程进行绑定(setTtlValuesTo);

那么在执行完setTtlValuesTo后,如果当前线程和调用线程并不是同一线程 (因为使用CallerRunsPolicy拒绝策略或ForkJoinPool导致调用线程和被调用线程数同一个) 的情况下,那么会发生内存泄漏吗?

下面简单地说明一下restore - restoreTtlValues代码逻辑,对于(多个)TransmittableThreadLocal/TTL实例持有的(多个)对象,即上下文(Context)的变化情况,以解释是否有内存泄漏的问题。

恢复过程涉及的上下文

restore - restoreTtlValues恢复过程 涉及2份上下文:

  • 在恢复之前的(多个)TTL实例持有的(多个)值,称为before context
  • 在恢复之后的(多个)TTL实例持有的(多个)值,称为after contextbackup context

对于任意一个在before contextTTL实例X的恢复过程

对于任意一个在before contextTTL实例X,恢复过程分成2个情况:

  • after context不存在
    • 恢复过程 即是 清除掉 TTL实例X持有的值
    • 对应代码 iterator.remove(); threadLocal.superRemove();
    • 清除操作 不会带来 内存泄漏问题。
  • after context存在
    • 恢复过程 即是 设置 TTL实例X持有的值 成 after context中对应的值。
    • 对应代码 setTtlValuesTo(backup);
    • 因为设置的值是业务需要的、要恢复的, 也不会带来 内存泄漏问题。

上面恢复过程中内存/对象持有的变化情况的说明,与『当前线程和调用线程是否是同一线程』无关,都是一样的;
即都没有技术上的内存泄漏问题。 @StrongBanana

# 上面的内容是展开解释了代码实现的逻辑。

private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
    // call afterExecute callback
    doExecuteCallback(false);

    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocal<Object> threadLocal = iterator.next();

        // clear the TTL values that is not in backup
        // avoid the extra TTL values after restore
        if (!backup.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // restore TTL values
    setTtlValuesTo(backup);
}

欢迎更好的解释说明

你看看说明清楚了不? @StrongBanana
说的不清楚的地方(这样的问题可能不容易说得深入浅出),
欢迎反馈、期待你能再写一下~ 比如能写篇文章来展开说一下。 💕

这样的问题 相信大家也都挺关心的~


这个 Issue 先Close了~ 欢迎更好的讲解 💕 @StrongBanana @大家

@oldratlee oldratlee changed the title com.alibaba.ttl.TransmittableThreadLocal.Transmitter#restore 方法是否会导致内存泄漏? TransmittableThreadLocal.Transmitter#restore 方法是否会导致内存泄漏? Oct 24, 2021
@oldratlee oldratlee self-assigned this Oct 24, 2021
@nicky1
Copy link

nicky1 commented Nov 3, 2022

你好,项目中需要在线程池中传递上下文,正要引入TTL来解决这个问题。
有个疑问,麻烦帮忙解疑下。
image

  1. TransmittableThreadLocal 类的 restoreTtlValues(), 会有清理调用线程的时机吗
  2. 不清理 调用线程持有的上下文变量,不会内存溢出吗

@oldratlee
Copy link
Member

oldratlee commented Nov 4, 2022

  1. TransmittableThreadLocal 类的 restoreTtlValues(), 会有清理调用线程的时机吗

不会。 @nicky1
因为这样的使用方式在调用线程这一级没有restore操作。

pipeline式拦截

可以使用pipeline式拦截(你上面的示例中是pre/post式拦截),
然后通过runSupplierWithCaptured
可以还原(清理)调用线程,
并且是对所有的TTL实例完成传递与还原(清理)。

示意代码:

public Object invoke(...) {
    final Object captured = Transmitter.capture();
    return Transmitter.runSupplierWithCaptured(captured, () -> {
        // get user and bind it into context
        UserContext.bindUser(...);

        pipeline.invoke(...)
    });
}

pre/post式拦截

示意代码:

(对于AsyncHandler异步处理是否正确,要具体了解Spring,我没研究过 😄)

public class ContextInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // get user and bind it into context
        UserContext.bindUser(...);
    }

    @Override
    public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserContext.clearUser();
    }
}

@nicky1
Copy link

nicky1 commented Nov 7, 2022

@oldratlee 你好,在本地测试过程中,遇到一个问题,为了要清理调用线程的ThreadLocal,所以在拦截器中的afterCompletion()中做了清理。

类似的示例代码,使用spring 拦截器:

public class UserInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userId = request.getParameter("userId");
        if (StringUtils.isNotBlank(userId)) {
            UserInfo userInfo = new UserInfo();
            userInfo.setUserId(Integer.valueOf(userId));
            ThreadContext.bindUser(userInfo);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        ThreadContext.unBindUser();
        super.afterCompletion(request, response, handler, ex);
    }
}

业务入口代码:会发现 偶尔取到的userId值是0

@GetMapping("/test/ttl/pool")
    public ResponseEntity ttlPool(@RequestParam Integer userId) {
        CompletableFuture.runAsync(() -> {
            // 模拟业务耗时
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {

            }
            UserInfo user = ThreadContext.getUser();
            Integer contextValue = Objects.isNull(user) ? 0 : user.getUserId();
            log.info("传入的userId和从线程上下文获取的是否一致,flag:{},传入userId:{},上下文获取:{}", (userId.equals(contextValue)), userId, contextValue);
        }, commonThreadPool);
        return ResponseEntity.ok().build();
    }

猜测是 因为拦截器中先执行了清理,等到线程异步执行时,主线程中的threadlocal 已经被清理掉了。
不知道你们有遇到这个问题吗

@oldratlee
Copy link
Member

oldratlee commented Nov 7, 2022

猜测是 因为拦截器中先执行了清理,等到线程异步执行时,主线程中的threadlocal 已经被清理掉了。

关于Spring (MVC)的拦截器及其执行方式,你了解研究一下Spring的使用。❤️

因为这些与TTL无关 & 我没有了解过,
这个 TTL的 issue 里就不再继续讨论了。 @nicky1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants