为了账号安全,请及时绑定邮箱和手机立即绑定

深入并发之(二) ThreadLocal源码与内存泄漏相关分析

标签:
Java

深入并发二 ThreadLocal源码与内存泄漏相关分析

这篇文章的主要内容是介绍ThreadLocal类使用方法,源码实现,以及实际应用。ThreadLocal实际上是在多线程编程的过程中,每个线程用来保存局部变量的一个类,用这个类保存的变量在属于各个线程独有,不会互相影响,那么我们就可以实现不同线程保存同一个变量的不同值。

ThreadLocal的使用

ThreadLocal的使用十分方便,下面给出一个使用的例子,实现同一个变量在不同线程中有着不同的值,同时,这两个值不互相影响。

public static void main(String[] args) {
       ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {           return 0;
       });       
       new Thread(() -> {           value.set(10);           //Thread-0 10
           System.out.println(Thread.currentThread().getName() + " " + value.get());
       }).start();       
       new Thread(() -> {           //Thread-1 0
           System.out.println(Thread.currentThread().getName() + " " + value.get());           value.set(3);           //Thread-1 3
           System.out.println(Thread.currentThread().getName() + " " + value.get());
       }).start();
   }

上面的代码中,我们定义了一个变量value,这个变量在不同线程中有不同的值,所以我们使用ThreadLocal,初始化这个值为0。上面的代码十分简单,就不做详细讲解了。

ThreadLocal源码分析

下面我们来分析一下ThreadLocal的底层实现。

实际上,每个Thread对象都持有一个ThreadLocalMap的对象,里面保存了所有ThreadLocal变量,这个map的key是ThreadLocal变量对象,而值就是这个线程中ThreadLocal对应的值。

ThreadLocalMap实际使ThreadLocal的一个静态内部类。

下面我们先来分析方法set()

public void set(T value) {    //获取当前线程
    Thread t = Thread.currentThread();    //获取线程所持有的map对象
    ThreadLocalMap map = getMap(t);    if (map != null)        //以当前ThreadLocal为key,将value值加入map中
        map.set(this, value);    else
        //如果map对象还没有,那么调用初始化方法,并且将值插入
        createMap(t, value);
}ThreadLocalMap getMap(Thread t) {    //获取线程对象持有的map对象
    return t.threadLocals;
}void createMap(Thread t, T firstValue) {    //初始化threadLocals
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

然后我们来分析一下get()方法

public T get() {    //获取当前线程
    Thread t = Thread.currentThread();    //获取线程所持有的map对象
    ThreadLocalMap map = getMap(t);    if (map != null) {        //取出map中key为当前ThreadLocal对象的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);        //如果存在,直接返回value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;            return result;
        }
    }    //如果上面没有返回,证明还没有赋值,那么调用初始化的方法
    return setInitialValue();
}private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);    if (map != null)
        map.set(this, value);    else
        createMap(t, value);    return value;
}

在这里我们可以看到如果我们调用过ThreadLocal对象的set方法给对象赋值的话,这是调用get方法去取值,会调用方法setInitialValue。所以,一般我们在初始化ThreadLocal对象的时候,会重写方法initialValue(),这样就不会发生get方法返回值为null的情况。

同时在java8之后,我们也可以采用最开始的例子中的方法来初始化ThreadLocal对象。

ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {    return 0;
});

ThreadLocalMap分析与ThreadLocal导致内存泄露的问题分析

这里我们不去分析ThreadLocalMap中方法的具体实现,它的大部分功能和一个普通的map相似,我们主要是要分析一下ThreadLocal导致内存泄漏的原因。

首先,给出关键部分的代码

static class Entry extends WeakReference<ThreadLocal<?>> {        /** The value associated with this ThreadLocal. */
        Object value;        Entry(ThreadLocal<?> k, Object v) {            super(k);
            value = v;
        }
    }

注意,ThreadLocalMap中的key实际上是ThreadLocal对象的弱引用。

那么什么是弱引用呢,既然有弱引用必然就有强引用。

实际上强引用就是我们正常使用new关键字创建的引用,** 弱引用指的其实是WeakReference关键字包裹的引用,在GC的过程中,如果一个对象只有弱引用指向它的时候,这个对象就已经可以被GC回收了,而一个强引用只有当所有引用都不存在的时候才可以被回收。**

那么,在map中为什么要使用弱引用这种方式呢?请大家想想一种情况,我们有一个对象,这个对象有一个引用A,并且这个对象作为map的key存在,那么当我们不再使用这个对象的时候,我们将引用置为null,这是,假设map中的引用是强引用,那么由于map中依然有这个对象的引用,那么这个对象不能够被GC回收,这显然不是我们想要看到的场景,所以,一般来说map中的key一般使用弱引用,这样,当对象只有这一个引用的时候就可以及时被GC回收。

  • 因此,我们就有HashMapWeakHashMap两个类,大家可以后续了解一下,两者的主要区别就在于key是强引用还是弱引用。 *

下面转回正题,关于ThreadLocal导致内存泄漏的问题。

在这里,key值实际上是ThreadLocal变量的弱引用,所以当我们的key变为空的时候这个引用就不存在了,那么我们也就无从得到value的值,这是value的值就变成了无法访问的值。

ThreadLocalMap中实际上已经考虑到了这个问题,当我们调用ThreadLocal中set、get和remove方法的时候,实际上是会检查key为null的情况,将这些内容清掉。

当线程的生命周期结束的时候后,实际上所有的ThreadLocalMap都会被回收,因此,这种情况下不会造成内存泄漏。

这里引用StackOverFlow中一位答主给出的情况,详情见 java - ThreadLocal & Memory Leak - Stack Overflow

这里给出翻译。

举一个例子:
有一个服务器有一个线程池,这些线程会一直存活知道服务器停止。
一个web应用在一个类中使用了一个static的ThreadLocal来存放一些线程局部变量,这个变量是web应用中里一个类的对象(SomeClass)。这些操作实在一个线程中进行的。
根据定义,一个ThreadLocal的引用会一直存活,知道拥有这个对象的线程死亡或者ThreadLocal对象本身是不可达的。
如果web应用在关闭之前没有成功清除ThreadLocal的引用,那么这时会发生十分糟糕的事情:
因为线程不会死亡,并且ThreadLocal对象依然指向着的引用是static的,那么,虽然应用已经停止了,ThreadLocal对象依然指向着SomeClass的对象(一个web应用中的类)
这种情况的结果就是,web应用中的classloader不会被GC,这就意味着web应用中所有的类(以及所有的静态类)都仍然被装载(这会影响到PermGen)
每一次reload应用都会增加PermGen的使用,这样就会导致permgen leak

相信上面的解释已经十分清晰了。下面给出tomcat中出现的例子,这个bug已经被官方修复了。

MemoryLeakProtection - Tomcat Wiki

至此,我们对ThreadLocal的了解已经十分深入了,在我们使用ThreadLocal类的时候,一定要十分注意,防止发生内存泄漏。

原文出处:https://www.cnblogs.com/qmlingxin/p/9412061.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消