深入分析ThreadLocal

在上一篇(看懂ThreadLocal,有这一篇就够了)文章中,我们聊了聊ThreadLocal的基本原理。今天,我们再对ThreadLocal更高级的知识进行学习,主要包含两部分内容:
1、InheritableThreadLocal
2、ThreadLocal内存泄露问题

1.InheritableThreadLocal

首先,我们先看一个例子:

public class ThreadLocalDemo {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("main Thread");
        Thread thread = new Thread(() -> {
            System.out.println("子线程value:" + threadLocal.get());
        });

        thread.start();
        System.out.println("主线程value:" + threadLocal.get());
    }
}

上面代码在主线程中设置了threadLocal变量,并将其值设置为“main Thread”;然后启动了一个子线程thread, 并试图在子线程中获取在主线程中设置的threadLocal变量的值。但是结果如下:

主线程value:main Thread
子线程value:null

通过上一篇文章的介绍,对上面的结果我们也就非常容易理解了。

每个线程都有自己单独的ThreadLocalMap,所以子线程在调用threadLocal.get()方法时访问的是自己的ThreadLocalMap, 这个ThreadLocalMap和主线程的ThreadLocalMap是两个map, 所以子线程肯定是获取不到主线程设置的ThreadLocal变量的值。

那有没有办法能够让子线程访问到主线程设置的threadLocal变量的值呢?
答案就是用InheritableThreadLocal。

将上面的代码稍微做一点改变,如下所示:

public class ThreadLocalDemo {

    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("main Thread");
        Thread thread = new Thread(() -> {
            System.out.println("子线程value:" + threadLocal.get());
        });

        thread.start();
        System.out.println("主线程value:" + threadLocal.get());
    }
}

上面代码,我们将ThreadLocal替换为InheritableThreadLocal,其他一切都未变。运行结果如下:

主线程value:main Thread
子线程value:main Thread

可以看到这次在子线程能够访问到父线程设置在InheritableThreadLocal变量中的值。

那我们不禁要问:InheritableThreadLocal是如何做到这一切的呢?

让我们进入进入get()方法一探究竟:

public class ThreadLocal<T> {

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
    }
    
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

我们看到InheritableThreadLocal.get()方法就是调用了父类ThreadLocal.get()方法。InheritableThreadLocal继承了ThreadLocal类,但是InheritableThreadLocal重写了父类的getMap()方法, InheritableThreadLocal源代码如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }
    
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
   
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在父类ThreadLocal的get()方法返回的是线程t的threadLocals成员变量, 而在子类InheritableThreadLocal的get()方法返回的是线程t的inheritableThreadLocals成员变量。

在Thread类中,我们也确实看到有这两个成员变量:

class Thread implements Runnable {

    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

就是这一点不同,导致了上面两个示例代码得到不同的结果。

也就是说,当我们使用InheritableThreadLocals.get()时,是从线程t的成员变量inheritableThreadLocals中获取对应的变量值,当使用ThreadLocal.get()时,是从线程t的成员变量threadLocals中获取对应变量的值。

进一步推出:子线程中的inheritableThreadLocals获取到了了父线程的inheritableThreadLocals中变量的值,而子线程中的threadLocals却获取不到父线程的threadLocals中变量的值。

那父线程是什么时候将自己成员变量inheritableThreadLocals中保存的变量值传递给了子线程的成员变量inheritableThreadLocals呢?

我们看下new Thread()这行代码发生了什么:

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

        // 注意区分:currentThread()获取到的是主线程,因为new Thread()方法是在主线程中执行的
        Thread parent = currentThread();
        
        ....忽略其他代码
        
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
              this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
 
}

通过上面代码,我们清楚地看到,在主线程中调用new Thread()方法去创建子线程时,在内部会将主线程的inheritableThreadLocals成员变量中保存的值传递给子线程的成员变量inheritableThreadLocals。

这就是为什么使用InheritableThreadLocal, 能够使得子线程访问到父线程的InheritableThreadLocal设置的值。

还有一点需要注意:
一旦子线程被创建以后,再操作父线程中的InheritableThreadLocal变量,那么子线程是不能感知的。因为父线程和子线程还是拥有各自的inheritableThreadLocals成员变量,只是在创建子线程的“一刹那”将父线程的inheritableThreadLocals复制给子线程,后续两者就没啥关系了。

通过以下代码,我们可以验证上面的结论:

public class ThreadLocalDemo {

    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("main Thread");
        Thread thread = new Thread(() -> {
            System.out.println("子线程value:" + threadLocal.get());
        });

        threadLocal.set("main Thread update");

        thread.start();
        System.out.println("主线程value:" + threadLocal.get());
    }
}

上面代码在主线程创建完子线程后,更改了主线程InheritableThreadLocal中保存的值。

运行结果如下:

主线程value:main Thread update
子线程value:main Thread

我们看到,确实在创建完子线程后,再操作父线程中的InheritableThreadLocal变量,那么子线程是不能感知的。

2.ThreadLocal内存泄露问题

到目前为止,我们基本上对ThreadLocal相关知识都掌握得差不多了。但是有一个点,我们一直没讲到。

看过ThreadLocal源码的人都应该知道,ThreadLocalMap是以ThreadLocal的弱引用作为key,那你有没有想过设计者为什么要把ThreadLocal的弱引用作为key呢?

这就涉及到的弱引用的作用和内存泄露问题。

java中的对象引用分为强引用、弱引用、软引用、虚引用。对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。

再来说说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。

因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。

再回到上面这个问题上来:
如果使用强引用,当ThreadLocal不再使用需要回收时,由于发现某个线程中的ThreadLocalMap存在该ThreadLocal的强引用,从而使得GC无法回收,导致内存泄露。

因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。

既然通过ThreadLocal的弱引用作为key, 可以避免内存泄露,那通常说的ThreadLocal的内存泄露是怎么回事呢?

我们看下ThreadLocalMap中的Entry类:

static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

我们看到虽然Entry的key(threadLocal)是弱引用,但是value本身是强引用。

这就导致,假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry,导致内存泄漏。

其实ThreadLocal已经帮我们尽量避免了内存泄露,在我们每次调用threadLocal的get()、set()、remove()方法时,threadLocal都会将key为null的Entry清除掉,从而避免了内存泄露。具体代码这里就不贴了。

但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。这就引出了ThreadLocal的最佳实践。

在上一篇文章中,我也说了使用完ThreadLocal变量要及时调用ThreadLocal.remove()方法清理掉保存的变量,现在看来一方面能够防止信息错乱(由于线程复用),另一方面也能够避免出现内存泄露的问题。

好了,ThreadLocal就讲到这了,希望各位能从中有所收获,没有浪费各位的时间:)

搜索并关注公众号「聊谈说侃」,获取更多精彩文章,我们一起共同进步…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值