面试之必问ThreadLocal内存泄漏问题

1、什么是ThreadLocal?

一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关,其思路是为每一个线程创建一个单独的变量副本,从而每个线程都可以独立地改变所拥有的变量副本,而不会影响其他线程所对应的副本。

ThreadLocal不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。

2.ThreadLocal的四个方法

(1) void set(Object value)

设置当前线程的线程局部变量的值。

(2) public Object get()

该方法返回当前线程所对应的线程局部变量。

(3) public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

(4) protected Object initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

public final static ThreadLocal RESOURCE = new ThreadLocal();RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

3.使用方法

public class ThreadLocalDemo {
    
    
    static ThreadLocal<Integer> countLocal = ThreadLocal.withInitial(()->0);
    static ThreadLocal<Integer> countLocal2 = ThreadLocal.withInitial(()->0);
    public static void main( String[] args ) {
    
    
        Thread[] threads = new Thread[3];
        for(int i=0;i<threads.length;i++){
    
    
            threads[i] = new Thread(()->{
    
    
                int count = countLocal.get();
                countLocal.set(count + 5);
                int count2 = countLocal2.get();
                countLocal2.set(count2+10);
                System.out.println(Thread.currentThread().getName() +":"+countLocal.get()+"->"+countLocal2.get());
            },"thread-"+i);
        }
        for(Thread thread :threads){
    
    
            thread.start();
        }
    }
}

3.1 存储结构

在这里插入图片描述

4.源码分析

4.1ThreadLocal的核心机制

在这里插入图片描述

  • 每个Thread线程内部都有一个Map。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

4.2 ThreadLocalMap

实现线程隔离机制的关键

每个Thread内部都有 一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。

提供了一种用健值对方式存储每一个线程的变量副本的 方法,key为ThreadLocal对象,value则是对应线程的 变量副本。

public class Thread implements Runnable {
    
    
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

在这里插入图片描述

  • 可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。
  • Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

在这里插入图片描述
ThreadLocalMap的问题

在这里插入图片描述
ThreadLocal的原理:每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal本身,Value是真正存的线程变量Object。

也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。注意上图的虚线,它代表一个弱引用类型,而弱引用的生命周期只能存活到下次GC前。

建议

每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

4.3 set 方法

在这里插入图片描述

  • 获取当前线程的成员变量map
  • map非空,则重新将ThreadLocal和新的value副本放入到map中。
  • map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。

4.4 get方法

在这里插入图片描述
1.获取当前线程的 ThreadLocalMap 对象threadLocals
2.从 map 中获取线程存储的 K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。

4.5 remove()方法

在这里插入图片描述

5. ThreadLocal为什么会内存泄漏

在这里插入图片描述

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存,由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

我们先看看ThreadLocal的get方法的底层实现:

在这里插入图片描述

在调用map.getEntry(this)时,内部会判断key是否为null,继续看map.getEntry(this)源码

在这里插入图片描述
在getEntry方法中,如果Entry中的key发现是null,会继续调用getEntryAfterMiss(key, i, e)方法,其内部回做回收必要的设置,继续看内部源码:

在这里插入图片描述
注意k == null这里,继续调用了expungeStaleEntry(i)方法,expunge的意思是擦除,删除的意思,见名知意,在来看expungeStaleEntry方法的内部实现:

在这里插入图片描述
注意这里,将当前Entry删除后,会继续循环往下检查是否有key为null的节点,如果有则一并删除,防止内存泄漏。

6. 如何避免泄漏

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

尽量在代理中使用 try-finally 块进行回收。

objectThreadLocal.set(userInfo);
try {
    
    
// ...
} finally {
    
    
objectThreadLocal.remove();
}

7.为什么ThreadLocalMap的key是弱引用呢?

key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

史上最全的并发编程脑图:https://www.processon.com/view/5d43e6cee4b0e47199351b7f

猜你喜欢

转载自blog.csdn.net/fd2025/article/details/108448648
今日推荐