ThreadLocal内存泄露问题&&key的弱引用相关问题总结

ThreadLocal的内存泄露是什么?如何解决

1、ThreadLocal是什么?

ThreadLocal也就是线程本地变量。如果你创建了⼀个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的⼀个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面 的变量,从而起到线程隔离的作用,避免了线程安全问题。

在多线程编程中,ThreadLocal 是一个常用的工具,用于在每个线程中维护独立的变量,避免了线程间的数据共享问题。然而,使用不当时,ThreadLocal 可能引发内存泄露,这是一个开发者们常常需要面对的难题。本文将深度剖析 ThreadLocal 内存泄露的原因,探讨解决方案,以及如何规避潜在的风险。

2. 内存泄露是如何发生的?

ThreadLocal造成内存泄露的问题

2.1 强引用导致的内存泄露

ThreadLocal 中存储的对象通常是通过强引用关联的。如果在 ThreadLocal 使用结束后没有手动调用 remove 方法清理数据,这些强引用将会一直存在,即便线程终止,对象也无法被垃圾回收,从而导致内存泄露。

2.2 线程池中的潜在问题

在使用线程池时,线程的生命周期不再由我们来控制。如果 ThreadLocal 的生命周期超过了线程的生命周期,就可能导致线程池中的多个任务共享 ThreadLocal 中的数据,引发意外的结果。

简单概括:
ThreadLocal可能会导致内存泄露,主要原因在于ThreadLocalMap的设计。ThreadLocalMap是ThreadLocal的内部类,用于存储每个线程的本地变量。ThreadLocalMap的键是对ThreadLocal对象的弱引用,而值是用户真正需要存储的对象。这就导致了一个问题:在ThreadLocal对象没有外部强引用时,这个对象就会被垃圾回收器回收,但是ThreadLocalMap中对应的value却无法被回收,因为ThreadLocalMap的生命周期跟Thread一样长,如果Thread一直不死,那么这个value就会一直存在一直占用内存,这就产生了内存泄露。

 3. 如何避免内存泄露?

3.1 及时清理 ThreadLocal

在使用完 ThreadLocal 后,应该及时调用 remove 方法清理数据。这一般建议放在使用完 ThreadLocal 的地方或线程结束时执行

public void someMethod() {
    try {
        threadLocal.set(someValue);
        // 其他操作
    } finally {
        threadLocal.remove();
    }
}
3.2 使用弱引用

为了更容易让对象被垃圾回收,可以使用 WeakReference 来包裹 ThreadLocal 中的对象。

private static final ThreadLocal<WeakReference<MyObject>> threadLocal = new ThreadLocal<>();

public void setThreadLocalValue(MyObject value) {
    threadLocal.set(new WeakReference<>(value));
}
 3.3 使用 InheritableThreadLocal 时的注意

InheritableThreadLocal 可以在父线程和子线程之间传递数据,但需要注意在不再需要的时候清理数据,以避免潜在的内存泄漏。

4. 内存泄露案例分析

考虑以下示例,在线程中使用 ThreadLocal 存储数据库连接:

public class DatabaseConnectionHolder {
    private static final ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();

    public static Connection getConnection() throws SQLException {
        Connection connection = connectionThreadLocal.get();
        if (connection == null || connection.isClosed()) {
            connection = createNewConnection();
            connectionThreadLocal.set(connection);
        }
        return connection;
    }

    private static Connection createNewConnection() throws SQLException {
        // 创建新的数据库连接
    }
}

在使用完数据库连接后,如果没有调用 remove 方法清理 ThreadLocal,就会导致连接对象被泄漏,因为线程池中的线程可能被复用,连接对象也就一直存在。

总结:

ThreadLocal 是一个强大的多线程编程工具,但在使用时需要格外小心,以避免引发内存泄露等问题。通过及时清理 ThreadLocal、使用弱引用以及注意线程池中的潜在问题,我们可以更安全地使用 ThreadLocal,确保应用程序的性能和稳定性。同时,利用内存分析工具和单元测试,可以更早地发现和解决潜在的内存泄露问题。希望通过本文的讨论,读者能更深入地理解 ThreadLocal 内存泄露问题,并在实际开发中避免相关风险。

ThreadLocal的key为什么设置成弱引用?

面试连环炮

先来体验一下关于ThreadLocal的连环炮:

1、ThreadLocal是什么?项目中用到过吗?
2、ThreadLocal的结构是怎么样的?
3、使用ThreadLocal需要注意哪些问题?
4、为什么key要设置成弱引用呢?
5、那为什么value不设置成弱引用呢?ThreadLocalMap不是持有对这个value的强引用,它还会被回收吗?

6、为什么会出现内存泄漏?你是怎么发现内存泄漏的?
7、怎么避免出现脏数据问题?
其实说实话,这几个问题都不难,稍微对ThreadLocal有点了解的都能答上来,关键在于第4个问题和第5个问题。

ThreadLocal面试总结

  • ThreadLocal原理

每个Thread对象都有一个 ThreadLocal.ThreadLocalMap 类型的成员变量threadLocals,存储本线程中所有的ThreadLocal对象及其对应的值(key 是当前ThreadLocal,value是对应的值)。ThreadLocalMap 由一个个entry组成,entry继承WeakReference,并且key是弱引用。

执行set方法时,ThreadLocal先获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将value值存入ThreadLocalMap对象中,get方法类似。由于每个线程各自私有ThreadLocalMap容器,这些容器互不影响,因此不会存在线程安全性问题。

  • ThreadLocal使用场景

对象跨层传递,避免多次传递
线程间数据隔离
数据库连接,session管理
用于事务操作,存储线程事务信息

  • ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突

当我们要往哈希表中插入一个数据时,通过哈希函数计算该值的哈希地址,当我们找到哈希地址时却发现该位置已经被别的数据插入了,那么此时我们就找紧跟着这一位置的下一个位置,看是否能够插入,如果能则插入,不能则继续探测紧跟着当前位置的下一个位置。

key为什么要设置成弱引用?

先来看看ThreadLocalMap对key和value的构造:

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

在set方法中,会将key和value以entry对象进行存储,key就是我们的Threadlocal引用,value就是我们要设置的值。

这里我们观察Entry这个类就会发现,它继承了WeakReference类,并且在构造方法中,将key设置成了弱引用,而value则是强引用。

为什么要这样做?

要知道,ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。

为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉,也就是说:假如某个用户A执行方法时产生了一份threadlocalA,然后在很长一段时间都用不到threadlocalA时,作为弱引用,它会在下次垃圾回收时被清理掉。

而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。

(跟踪Entry源码这个类就会发现,它继承了WeakReference类,并且在构造方法中,将key设置成了弱引用,而value则是强引用。
ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉)

value为什么不设置成弱引用?

回答完上面的问题,面试官紧接着就会问你:

“那照你的说法,value也应该设置成弱引用才对呀?不然不是一样会发生内存泄漏?”

对于这个问题,如果没有仔细思考,猝不及防下可能就会被问懵,对啊,既然key设置成弱引用的好处这么明显,那为什么value不也设置成弱引用呢?

我们来看下面一段代码:

public static void main(String[] args) throws Exception {
    Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
    WeakReference<Integer> key = new WeakReference<>(666);
    WeakReference<Integer> value = new WeakReference<>(777);
    map.put(key,value);
    System.out.println("put success");
    Thread.sleep(1000);
    System.gc();
    System.out.println("get " + map.get(key).get());
}

这时候,我在方法里执行我的代码时居然会出现空指针?

可能有些小伙伴不太懂,明明有value这个强引用啊?

因为value这个强引用是指向new WeakReference<>(777)的,也就是说,777这个才是我们真正存进去的对象,这时候它已经不存在任何强引用了,gc后它会被清理掉,所以为什么value为什么不设置成弱引用的原因:就是避免null空指针


部分内容参考相关链接整理:                    
原文链接:https://blog.csdn.net/qq_35971258/article/details/135644897

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值