探究-ThreadLocal内存泄露问题

探究这个问题之前,先来说说ThreadLocal是什么东西?

ThreadLocal 翻译过来就是线程本地。它存储的内容只在当前线程可见,其他线程则访问不到。

下面看一段代码方便理解:

 代码解读:

        1. 定义成员变量ThreadLocal

        2. 定义成员变量User对象

        3. 定义线程1将User对象set进ThreadLocal中

                线程1 通过get获取对象并打印

        4. 定义线程2从ThreadLocal中get获取对象

看了上面的控制台输出:线程1可以get到,线程2无法get到。结果会不会和你想的不一样呢?

既然ThreadLocal可以放进对象,是否能把它比作一个容器呢?

但拿我们平时常用的List来看的话,线程1对List存值,线程2自然可以get到,因为List是个容器嘛。可为什么ThreadLocal不行呢?

带着这些疑问,脑海里不禁有一个初步判断:无论哪一个线程对ThreadLocal存值,那么这个值仅限于当前线程使用~

可同一个ThreadLocal这个对象怎么做到不同线程存值,对数据进行隔离呢?让我们点开set方法看看究竟!

    public void set(T value) {
        // 获取正在被执行的线程信息
        Thread t = Thread.currentThread();
        // 通过获取的线程信息进行getMap操作,返回值是ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
      
        if (map != null)
            // 如果获取的ThreadLocalMap 不为null,则直接进行存值
            map.set(this, value);
        else
            // 反之则调用了创建Map的方法
            createMap(t, value);
    }
看了上面的源码,是不是能理解一点~  原来ThreadLocal的set方法 只是往一个Map里存入key-value

让我们点进getMap方法看看做了什么:

没想到啊...getMap方法只是返回Thread的一个成员变量threadLocals

继续跟进,看看这threadLocals究竟是什么东东

点进来以后,原来threadLocals 只是Thread类的一个成员变量,类型是ThreadLcalMap 。

看到这里是不是就解答了上面的疑问:

        为什么同一个ThreadLocal对象 不同线程存入的值,只在当前线程可见?

        原因很显而易见了,每次ThreadLocal的set方法  先获取当前执行的线程信息,通过这个线程信息获取到当前执行的线程自己的成员变量ThreadLcalMap ,然后把值存进去。那么别的线程肯定就不能访问另外一个线程自己的Map了。 如图所示~

ThreadLocal有什么用呢?

        了解Spring的小伙伴应该知道Spring有一个事务注解 @Transactional,当我们在方法上加上这个注解以后,无论进行几次增删改,都是带事务的对数据库进行操作。那么它是怎么做到的呢?

        大家都应该知道,当我们的程序每一次对数据库操作时都是一个新的数据库连接,如果数据库连接都不同的话,那事务就不复存在了。说白了,只要保证增删改 都是使用的一个数据库连接,那就可以保证事务。说到这里大家是不是好像理解了什么~

        没错!Spring的 @Transactional 内部就是使用了ThreadLocal 保存的数据库连接,而ThreadLocal保存的东西只有在当前线程可见,所以单个服务进行无论进行多少次增删改,实际上都是去ThreadLocal使用get方法获取同一个数据库连接进行操作,那么事务也就实现了~


简单介绍了ThreadLocal是什么以后,现在来探究ThreadLocal的内存泄漏问题

ps: ThreadLocal的内存泄漏问题,需要有JAVA引用类型的基础,如果不太清楚的小伙伴可以先看下我的这篇帖子~  探究- JAVA四种引用类型:强软弱虚引用

现在进入正题

JAVA的引用类型有一个弱引用,而ThreadLocal内部就是使用的弱引用。

接下来跟进一下源码看看ThreadLocal的set方法:

         可以看到调用set 往ThreadLocal中存值时,也就是map.set(this, value); 这一段。value是需要存入的值,而key却是this,这个this指代当前类,也就是当前的ThreadLocal。

        所以就说明,如果要往一个线程的ThreadLocalMap 要存入多个键值对,一个键值对 = 一个ThreadLocal ,看如下代码:

public class ThreadLocal_0 {
    // 第一个ThreadLocal对象
    static ThreadLocal<User> tl = new ThreadLocal<>();
    // 第二个ThreadLocal对象
    static ThreadLocal<String[]> tl2 = new ThreadLocal<>();

    // 提前定义好的一个User对象
    static User u = new User();

    public static void main(String[] args) {
        Thread t1 =  new Thread(() -> {
            tl.set(u);
            tl2.set(new String[]{"hello,world!"});
        });
    }
}

开头我说ThreadLocal内部就是使用的弱引用,实际上就是ThreadLocal的set方法存值时的key 使用弱引用指向当前的ThreadLocal。继续看map.set()的源码:

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

既然是属于Map,那么Entry是必不可少的,看源码也是有Entry的对象,Entry则是键值对的形式。那么让我们继续跟进Entry查看: 

些许有点拨开迷雾的感觉... ,ThreadLocalMap的键值对Entry对象继承了 WeakReference 弱引用。而第二个红框圈出的调用super方法,把k传了进去,实则就是使用弱引用指向堆内存的一个ThreadLocal对象当作map的键。

看下图方便理解:

         为什么要这么设计呢? 不妨这样思考,假设key也用强引用指向当前ThreadLocal的话,那么如果我这时候写 t1 = null ,按理说下次GC时,应该要把堆内存的new ThreadLocal() 这个对象进行回收才对,但此时我的key如果设计成强引用,显然GC无法对它进行回收,因为key还强引用指向它。这就会造成内存泄漏,所以ThreadLocal存值时,key采用弱引用。

        key使用弱引用的特点就很明显了(只要是GC回收,不管内存够不够,都会回收弱引用指向的对象),当我写 t1 = null , 下次GC回收时,就可以将new ThreadLocal() 这个对象会被回收掉。

        内存泄漏不单单只是上面的问题,上面所述的内存泄漏问题不需要关心,因为源码已经设计好并解决了。

        现在继续思考:既然key是虚引用指向,当写 t1 = null  ,下次进行GC回收时,这个堆中的ThreadLocal对象会被回收掉,那么key的引用就没有了,key会变成null,那么的内容还存在着,就会产生内存泄漏!,如下图:

         这时候你是不是在想,这怎么会造成内存泄漏呢,哪怕value指向的内容还存在 并且无法被回收,但只要线程结束,那么所有跟此线程相关的所有东西都会被回收掉了啊,为什么还会存在内存泄漏呢?

        是的,这样想是没问题,线程结束以后,跟它相关的一切确实就不存在了。

        但不妨思考一个问题,很多时候为了节省资源,我们都会用线程池来管理线程,大家知道线程池的特点,当一个线程使用结束后,会回到线程池等待下次使用。它并不会被回收掉,所以这时候线程使用的多了,每次使用都会造成value的内存泄漏,久而久之会引发OOM!这是个很严重的问题~ 

        解决方案是什么呢,其实很简单!当我们使用完ThreadLocal以后 一定要调用它的remove()方法,它会删除与当前ThreadLocal对应的键值对,从而解决了内存泄漏的可能。

ps: 到这里,枯燥的言论已经写完了 ^_^!,有哪些地方写的不足,欢迎指出,一起讨论~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值