ThreadLocal导致JVM内存泄漏原因探究

 更新时间:2023年04月19日 11:24:57   作者:胡尚  
ThreadLocal是JDK提供的线程本地变量机制,但若使用不当可能导致内存泄漏。正确的使用方式是在使用完后及时remove,或者使用弱引用等手段避免强引用导致的内存泄漏。在多线程编程中,合理使用ThreadLocal可以提高并发性能,但也需要注意其潜在的内存泄漏问题

为什么要使用ThreadLocal

在一整个业务逻辑流程中,为了在不同的地方或者不同的方法中使用同一个对象,但是又不想在方法形参中加这个对象,那么就可以使用ThreadLocal来保存

ThreadLocal最大的应用场景就是跨方法进行参数传递

ThreadLocal可以给每一个线程绑定一个变量的副本

使用ThreadLocal

ThreadLocal常用的方法其实也就下面几个

// 返回当前线程所对应的线程局部变量。
public T get() {}
// 设置当前线程的线程局部变量的值。
public void set(T value) {}
// 移除,当线程结束后,该线程thread对象中的局部变量将在下一次gc时回收,如果显示的调用此方法只是可以加快内存回收的速度
// 所以javase开发 普通new Thread()方式中,这个方法并不是必须要调用的
// 但是javaWeb开发中就必须显示调用,因为javaweb都是使用的线程池,并不是一个客户端来一个请求,thread线程对象用完就删除,而是会放回线程池中。
public void remove() {}
// 返回该线程局部变量的一个初始化
// protected方法,显然是为了让子类覆盖而设计的。这个方法在第一次调用 get()或 set(Object)时才执行,并且仅执行 1 次
protected T initialValue() {}

在具体使用的时候,我们ThreadLocal对象一定会定义成静态的,如果不定义成静态的那么其他地方如何通过这个ThreadLocal实例去Map中拿数据嘞?

而且如果是多个线程保存一个变量的副本,一个静态的ThreadLocal也足够了,因为它是作为多个map中的key存在的

简单使用案例

/**
 * @Description: 在一个方法中调用set()方法存值,在另一个方法中调用get()方法取值
 */
public class UseThreadLocalTest {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    /**
     * 创建一个线程类
     */
    public static class ThreadTest extends Thread{
        private Integer id;
        ThreadTest(Integer id){
            this.id = id;
        }
        @Override
        public void run() {
            threadLocal.set(Thread.currentThread().getName() + ":" + id);
            print();
        }
        public void print(){
            System.out.println(threadLocal.get());
        }
    }
    /**
     * 开三个线程
     */
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new ThreadTest(i).start();
        }
    }
}

// 输入结果如下
Thread-0:0
Thread-1:1
Thread-2:2

具体实现

ThreadLocal底层set()和get()方法的源码如下

// 存值时 map最终是存储在当前线程Thread t = Thread.currentThread()中的,是thread的一个成员变量
// map的key是当前threadLocal对象实例,value是要存的值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
// 取值时也是也是先从当前线程Thread对象中取出map
// 然后在从map中根据当前threadLocal对象实例作为key获取到entry对象
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();
}

为了提高性能,才没有采用加锁的方式,而是将map和各个线程thread对象进行关联,这样就避免了产生线程安全问题,也避免了加锁,提高了性能

我们接下来再来看看ThreadLocalMap它的实现,它类似于jdk1.7版本的hashmap,底层存储的是一个Entry对象的数组,初始容量也是16,存值时先用hash结果和数组长度取余得到数组下标位置,然后判断是否产生了hash冲突,然后使用开发定址法来处理。根据算法的不同又可以分为线性探测再散列、二次探测再散列、伪随机探测再散列。ThreadLocalMap它是使用的线性探测再散列法,如下所示

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

Entry对象中的key它是一个弱引用,Entry继承了WeakReference类,弱引用跟没引用差不多,GC会直接回收掉,不管内存是否足够都会回收

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

引发内存泄漏的原因

上面再介绍ThreadLocal基本使用api方法的时候也提到了,如果只是创建一个普通的线程Thread对象,是不会产生内存泄漏问题的。因为map是存储在Thread对象中,一个普通线程执行完了,那么这个线程的局部变量也就会被gc回收。

但如果结合到了线程池,一个Thread线程对象用完后放回线程池中,如果这个时候我们程序不显示的调用remove()方法,那么就会造成内存泄漏问题了。

因为Entry对象中的Key的弱引用,但是value还会存在,就会存在map中key为null的value

ThreadLocal 的底层实现中我们可以看见,无论是 get()set()在某些时 候,调用了 expungeStaleEntry() 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。

到此这篇关于ThreadLocal导致JVM内存泄漏原因探究的文章就介绍到这了,更多相关JVM内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot整合Mongodb实现增删查改的方法

    SpringBoot整合Mongodb实现增删查改的方法

    这篇文章主要介绍了SpringBoot整合Mongodb实现简单的增删查改,MongoDB是一个以分布式数据库为核心的数据库,因此高可用性、横向扩展和地理分布是内置的,并且易于使用。况且,MongoDB是免费的,开源的,感兴趣的朋友跟随小编一起看看吧
    2022-05-05
  • MyBatis通用的10种写法总结大全

    MyBatis通用的10种写法总结大全

    这篇文章主要给大家介绍了关于MyBatis通用的10种写法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-11-11
  • SpringBoot消息国际化配置实现过程解析

    SpringBoot消息国际化配置实现过程解析

    这篇文章主要介绍了SpringBoot消息国际化配置实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • SpringBoot 整合Redisson重写cacheName支持多参数的案例代码

    SpringBoot 整合Redisson重写cacheName支持多参数的案例代码

    这篇文章主要介绍了SpringBoot 整合Redisson重写cacheName支持多参数,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • JAVA基础之注解与反射的使用方法和场景

    JAVA基础之注解与反射的使用方法和场景

    这篇文章主要给大家介绍了关于JAVA基础之注解与反射的使用方法和场景的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • JDK1.8、JDK1.7、JDK1.6区别看这里

    JDK1.8、JDK1.7、JDK1.6区别看这里

    这篇文章主要为大家详细介绍了JDK1.8、JDK1.7、JDK1.6中的源码,对比阅读,发现修改问题以及改进点,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • Java中Range函数的简单介绍

    Java中Range函数的简单介绍

    这篇文章主要介绍了Java中Range函数的简单介绍,Java中的range方法用于返回IntStream和LongStream在函数参数范围内的顺序值
    2022-07-07
  • SpringMVC4+MyBatis+SQL Server2014实现数据库读写分离

    SpringMVC4+MyBatis+SQL Server2014实现数据库读写分离

    这篇文章主要介绍了SpringMVC4+MyBatis+SQL Server2014实现读写分离,需要的朋友可以参考下
    2017-04-04
  • Hadoop2.8.1完全分布式环境搭建过程

    Hadoop2.8.1完全分布式环境搭建过程

    本文搭建了一个由三节点(master、slave1、slave2)构成的Hadoop完全分布式集群(区别单节点伪分布式集群),并通过Hadoop分布式计算的一个示例测试集群的正确性。对hadoop分布式环境搭建过程感兴趣的朋友跟随小编一起看看吧
    2019-06-06
  • 浅谈Java枚举的作用与好处

    浅谈Java枚举的作用与好处

    下面小编就为大家带来一篇浅谈Java枚举的作用与好处。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-07-07

最新评论