ThreadLocal导致JVM内存泄漏的原因是什么

其他教程   发布日期:2025年03月17日   浏览次数:210

这篇“ThreadLocal导致JVM内存泄漏的原因是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“ThreadLocal导致JVM内存泄漏的原因是什么”文章吧。

为什么要使用ThreadLocal

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

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

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

使用ThreadLocal

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

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

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

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

简单使用案例

  1. /**
  2. * @Description: 在一个方法中调用set()方法存值,在另一个方法中调用get()方法取值
  3. */
  4. public class UseThreadLocalTest {
  5. public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
  6. /**
  7. * 创建一个线程类
  8. */
  9. public static class ThreadTest extends Thread{
  10. private Integer id;
  11. ThreadTest(Integer id){
  12. this.id = id;
  13. }
  14. @Override
  15. public void run() {
  16. threadLocal.set(Thread.currentThread().getName() + ":" + id);
  17. print();
  18. }
  19. public void print(){
  20. System.out.println(threadLocal.get());
  21. }
  22. }
  23. /**
  24. * 开三个线程
  25. */
  26. public static void main(String[] args) {
  27. for (int i = 0; i < 3; i++) {
  28. new ThreadTest(i).start();
  29. }
  30. }
  31. }

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

具体实现

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

  1. // 存值时 map最终是存储在当前线程Thread t = Thread.currentThread()中的,是thread的一个成员变量
  2. // map的key是当前threadLocal对象实例,value是要存的值
  3. public void set(T value) {
  4. Thread t = Thread.currentThread();
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. map.set(this, value);
  8. else
  9. createMap(t, value);
  10. }
  11. // 取值时也是也是先从当前线程Thread对象中取出map
  12. // 然后在从map中根据当前threadLocal对象实例作为key获取到entry对象
  13. public T get() {
  14. Thread t = Thread.currentThread();
  15. ThreadLocalMap map = getMap(t);
  16. if (map != null) {
  17. ThreadLocalMap.Entry e = map.getEntry(this);
  18. if (e != null) {
  19. @SuppressWarnings("unchecked")
  20. T result = (T)e.value;
  21. return result;
  22. }
  23. }
  24. return setInitialValue();
  25. }

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

我们接下来再来看看

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

Entry对象中的

  1. key
它是一个弱引用,Entry继承了
  1. WeakReference
类,弱引用跟没引用差不多,GC会直接回收掉,不管内存是否足够都会回收
  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. Object value;
  3. Entry(ThreadLocal<?> k, Object v) {
  4. super(k);
  5. value = v;
  6. }
  7. }

引发内存泄漏的原因

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

但如果结合到了线程池,一个Thread线程对象用完后放回线程池中,如果这个时候我们程序不显示的调用

  1. remove()
方法,那么就会造成内存泄漏问题了。

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

ThreadLocal 的底层实现中我们可以看见,无论是

  1. get()
  1. set()
在某些时 候,调用了
  1. expungeStaleEntry()
方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。

以上就是ThreadLocal导致JVM内存泄漏的原因是什么的详细内容,更多关于ThreadLocal导致JVM内存泄漏的原因是什么的资料请关注九品源码其它相关文章!