ThreadLocal内存泄漏分析

一、ThreadLocal内存泄漏分析

1.1 ThreadLocal实现原理

1.1.1、set(T value)方法

查看ThreadLocal源码的 set(T value)方法,可以发现数据是存在了ThreadLocalMap的静态内部类Entry里面

其中key为使用弱引用的ThreadLocal实例,value为set传入的值。核心源代码:

public void set(T value) {
    Thread t = Thread.currentThread();
    // ThreadLocalMap跟当前线程对象绑定,是线程对象中的一个成员属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
      map.set(this, value);
    else
      // 第一次调用的时候,将当前线程和value往下传。
      createMap(t, value);
}
​
void createMap(Thread t, T firstValue) {
    // 当前线程内部的变量 ThreadLocal.ThreadLocalMap threadLocals 设置为新建出来的对象。
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
​
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
      
      Object value;
​
      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }
    ...
}

1.2 ThreadLocal 内存泄漏的原因

引用原理图如下,实心箭头表示强引用虚线箭头表示弱引用

1、图中所示,当前线程强引用了ThreadLocalMap,而ThreadLocal为ThreadLocalMap的弱引用Key。

2、结合1.2节知识背景,如果ThreadLocal没有被外部强引用,当系统触发GC时,会将ThreadLocal对象回收掉,会导致ThreadLocalMap的Key为null,但是value还是被当前线程强引用,只有当Thread线程退出后,value的强引用链才会断开。

3、如果线程不结束(比如使用了线程池),则引用链(Thread -> ThreadLocalMap -> Entry -> value)一直存在,永远不会被回收,从而造成内存泄漏。

1.2.1 代码演示内存泄漏
public class ThreadLocalTest {
    public static void main(String[] args) {
        //二、TheadLocal内存泄漏
        //2.1、局部代码块中创建ThreadLocal后不引用它。
        createThreadLocal();
        //2.2、让GC回收不再被强引用,只有弱引用的TheadLocal对象
        System.gc();
        //2.3、查看线程成员属性ThreadLocalMap中 存入的键值对(key为null,而value还在,出现内存泄漏问题)
        Thread thread = Thread.currentThread();
    }
​
    public static ThreadLocal<String> createThreadLocal(){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("zs");
        return threadLocal;
    }
}
​

debug运行代码,查看当前线程的ThreadLocalMap中的数据,可以发现引用的Key已经被GC回收了,造成了内存泄漏

1.3 key为什么使用弱引用

即使没有手动删除key和value,ThreadLocal在没有被引用的时候也会被回收。即ThreadLocalMap的key为null,下一次ThreadLocalMap调用set()、get()、remove()方法的时候会清除没被回收的value。

1.3.1 代码演示清除没被回收的value
package com.adolesce.server.mutithread;
​
/**
 * @author Administrator
 * @version 1.0
 * @description: TODO
 * @date 2023/7/1 9:52
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        //三、TheadLocal没被强引用后,触发System.gc(),将key回收,设为null
        //3.1、创建ThreadLocal对象
        ThreadLocal threadLocal = createThreadLocal();
        //3.2、模拟threadLocal没被引用:断点时手动将thread中ThreadLocalMap value为【zs】对应的key设为null
        Thread thread = Thread.currentThread();
        //3.3、测试get、set、remove方法将key为null的value清除
        threadLocal.get();
        //3.4、查看ThreadLocalMap中value是否为null了
        thread = Thread.currentThread();
    }
​
    public static ThreadLocal<String> createThreadLocal(){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("zs");
        return threadLocal;
    }
}

调用get方法的时候,由于map中的value对应的key为null,通过当前ThreadLocal对象去获取是获取value是获取不到Entry,于是调用初始化value的方法,清除原来的value,如get源码:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) { // 为null跳出判断
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue(); // 调用初始化value的方法,清除原来的value
}

二、ThreadLocal的正确使用方法

1、每次使用完ThreadLocal都调用它的remove()方法清除数据。

2、将ThreadLocal变量定义成 private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能

通过 ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

package com.adolesce.server.mutithread;
​
/**
 * @author Administrator
 * @version 1.0
 * @description: TODO
 * @date 2023/7/1 9:52
 */
public class ThreadLocalTest {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
​
    public static void main(String[] args) {
        //一、TheadLocal使用
        System.out.println("主线程开启,线程ID:" + Thread.currentThread().getId());
        //1.1、向ThreadLocalMap中存入键值对
        setData();
        //1.2、从ThreadLocal中获取数据
        getData();
        //1.3、清除(通常在拦截器的afterCompletion()方法中进行清除)
        removeData();
    }
​
    private static void setData() {
        threadLocal.set("zhangsan");
        System.out.println("在线程"+Thread.currentThread().getId()+"中向ThreadLocal中存入了姓名");
    }
​
    private static void getData() {
        String name = threadLocal.get();
        System.out.println("在线程"+Thread.currentThread().getId()+"中从ThreadLocal中取了姓名:" + name);
    }
    
    private static void removeData() {
        threadLocal.remove();
    }
}
​

三、总结

1、内存泄漏原因:我们使用ThreadLocal过程中,如果ThreadLocal对象强引用断掉后,只剩弱引用,ThreadLocal对象会被回收,此时ThreadLocal中的key会变为null,而value没有被回收,同时又由于ThreadLocalMap是Thread中的成员属性,与Thread对象的生命周期是一样长,如果当前线程一直未被销毁,又没有手动删除对应key,这样就会导致value内存泄漏。

2、使用弱引用可以多一层value回收的保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set()、get()、remove()的时候会被清除。

3、因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal 内存泄露问题是指当使用ThreadLocal类时,如果没有正确地进行清理和处理,就有可能导致内存泄露的情况发生。这是因为ThreadLocal对象的生命周期与线程的生命周期相对独立,当线程结束时,ThreadLocal对象没有被垃圾回收,且其中存储的数据也无法被访问,从而导致内存泄露。 具体来说,ThreadLocal类通过操作ThreadLocalMap来存储每个线程的数据。当一个线程结束时,如果没有正确地清理ThreadLocal对象,那么ThreadLocalMap中与该线程相关的条目将无法被删除。这意味着,即使这些条目对应的线程不再活跃,它们却仍然占据着内存空间。 一种常见的导致ThreadLocal内存泄露的情况是在使用完ThreadLocal对象后未调用其remove方法进行清理操作。如果在一个长时间运行的线程中重复使用ThreadLocal对象,而不进行清理操作,就会导致ThreadLocalMap中的条目越来越多,从而造成内存泄露。 另外,当ThreadLocal对象被作为静态变量使用时,也容易出现内存泄露的问题。因为静态变量的生命周期很长,如果静态ThreadLocal对象没有被妥善处理,那么其中的数据也将无法被释放。 为了避免ThreadLocal内存泄露,应该养成良好的编程习惯,确保在使用完ThreadLocal对象后,及时调用其remove方法进行清理。另外,如果ThreadLocal对象被用作静态变量,也应该在不再使用时手动将其置为null,以便让垃圾回收器能够回收相关的内存空间。 参考资料: :可以发现问题,ThreadLocal已经被清理掉了,代表现在已经没有方式去访问当前ThreadLocal存到Map里的value数据了 。 :ThreadLocal就相当于一个访问工具类,通过操作ThreadLocal对象的方法 来操作存储在当前线程内部的ThreadLocalMap里的值 。 :一篇文章我们来分析一个JavaThreadLocal内存泄露的案例。分析问题的过程比结果更重要,理论结合实际才能彻底分析内存泄漏的原因。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leighteen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值