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

主线程执行完后会remove,但是子线程执行完成后会restore恢复本地变量,子线程中的值变量如何清除? #533

Closed
mingyang66 opened this issue Aug 5, 2023 · 6 comments
Assignees
Labels
❓question Further information is requested

Comments

@mingyang66
Copy link

mingyang66 commented Aug 5, 2023

如下:主线程执行完后会remove,但是子线程执行完成后会restore恢复本地变量,子线程的本地变量如果不主动删除会不会OOM?

public class Test {
    private static final ThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newSingleThreadScheduledExecutor();
        System.out.println(1 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
        CONTEXT.set("parent");
        System.out.println(2 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
        service.submit(TtlRunnable.get(new Runnable() {
            @Override
            public void run() {
                System.out.println(3 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
                CONTEXT.set("sub");
                System.out.println(4 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
            }
        }));
        System.out.println(5 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
        CONTEXT.remove();
        System.out.println(6 + "-" + Thread.currentThread().getName() + ":" + CONTEXT.get());
    }
}
@mingyang66 mingyang66 changed the title 主线程执行完后会remove,但是子线程执行完成后会restore恢复本地变量,子线程的本地变量如果不主动删除会不会OOM? 主线程执行完后会remove,但是子线程执行完成后会restore恢复本地变量,子线程中的值变量如何清除? Aug 5, 2023
@mingyang66
Copy link
Author

TransmittableThreadLocal内部本身定义了holder变量如下:

    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    return new WeakHashMap<>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
                }
            };

其中InheritableThreadLocal内部存储的是一个WeakHashMap类型,其键是一个WeakReference类型,如果没有强引用的话就会在下次GC的时候回收掉,而其value本身就是null,所以对于TTL本身来说是不存在内存溢出问题的;

对于子线程而言其值是存在于Thread线程的ThreadLocal.ThreadLocalMap inheritableThreadLocals变量中,而ThreadLocalMap内部的存储结构是一个WeakReference类型,如果没有强引用则会在下次GC的时候置为null,在下次使用的时候会主动清除掉key为null引用,从而避免OOM;

上述异步线程池中最外层主线程主动remove,内部无法主动remove,而不会造成OOM的原因是不是上述分析的 @oldratlee 帮忙解答下,谢谢

@oldratlee
Copy link
Member

oldratlee commented Aug 7, 2023

因为有「恢复(Transmitter.restore())」操作做了清除操作,
在运行完任务之后子线程(的ThreadLocal)不再持有上下文,
所以不会因为这个传递过程而引入内存泄露。 @mingyang66
注意:上面的过程 不包含「由Inheritable能力(InheritableThreadLocal) 带给子线程的上下文」这个情况。

对于Inheritable能力引起的「内存泄露」,有较多讨论,
可以看看网上的讨论、或这个库涉及「内存泄露」相关的issue。

TTL提供了关闭Inheritable的一些方法,具体参见TransmitableThreadLocal的JavaDoc。

@oldratlee oldratlee added the ❓question Further information is requested label Aug 7, 2023
@mingyang66
Copy link
Author

我看很多案例和issue中都没有调用TransmittableThreadLocal.remove方法移除上下文的操作,是不是因为TTL中使用WeakHashMap和ThreadLocal使用WeakRefrence的原因?GC的时候会自动回收?

@oldratlee
Copy link
Member

oldratlee commented Aug 8, 2023

我看很多案例和issue中都没有调用TransmittableThreadLocal.remove方法移除上下文的操作,是不是因为TTL中使用WeakHashMap和ThreadLocal使用WeakRefrence的原因?GC的时候会自动回收?

@mingyang66 上一条评论有原因的一些解释:

因为有「恢复(Transmitter.restore())」操作做了清除操作,
在运行完任务之后子线程(的ThreadLocal)不再持有上下文,
所以不会因为这个传递过程而引入内存泄露。 @mingyang66
注意:上面的过程 不包含「由Inheritable能力(InheritableThreadLocal) 带给子线程的上下文」这个情况。

如果想更多深入理解说明TTL的实现设计,

@mingyang66
Copy link
Author

mingyang66 commented Aug 9, 2023

非常感谢 @oldratlee 的回答,今天花了一天时间研究这一块,其实值传递的核心是

  • capture【获取父线程中的值,包括引用对象或普通对象】
  • replay【回放:备份、将父线程的值设置到子线程】
  • restore【会将子线程执行之前backup的值设置回子线程的ThreadLocal】;

如果通过如下关闭Interitable能力:

          @Override
          protected User childValue(User parentValue) {
              return initialValue();
          }

那么在子线程replay回放时backup就不存在,所有在restore恢复的时候子线程其实是不存在值恢复的;

如果不关闭Interitable能力,那么子线程继承了父线程的值【普通对象、引用类型】,在restore后会将子线程的值恢复成从父线程继承的值或引用类型,但是如果子线程执行结束了,那么子线程的ThreadLocal就不会持有父线程的上下文了,这是因为ThreadLocal的生命周期是和线程的生命周期相关联,线程结束,则ThreadLocal变量也会自动被GC销毁;

还有个问题,就是线程池复用的问题,如果不关闭线程的Interitable能力,则子线程继承父线程的引用对象,如果执行完成后被线程池复用,那么就会有一部分引用对象一直无法释放,如果这个量比较大的话有可能造成OOM,像这种问题又是如何解决的呢?

有空了帮忙解答下,谢谢!

@mingyang66
Copy link
Author

我在#521这个ISSUE中找到了想要的答案, @oldratlee 感谢

@oldratlee oldratlee self-assigned this Dec 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
❓question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants