ThreadLocal的使用场景与内存泄漏问题

1. ThreadLocal定义

ThreadLocal 是 Java 中的一个线程局部变量,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

java 复制代码
Runnable task = () -> {
    int value = (int) (Math.random() * 100); // 生成随机数
    threadLocalVar.set(value);  // 绑定到当前线程
    System.out.println(Thread.currentThread().getName() + " 设置的值:" + threadLocalVar.get());
};

Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);

thread1.start();
thread2.start();

核心数据结构

ThreadLocal 的核心实现依赖于两个类:ThreadLocal 类本身和 ThreadLocalMap 类。ThreadLocalMap 是 ThreadLocal 的一个静态内部类,每个Thread对象都有一个ThreadLocalMap类型的成员变量threadLocals。

为什么内部要使用ThreadLocalMap来存储本地变量?

  • 线程私有,避免线程之间的竞争
  • Map类型可以支持多个本地变量的存储
  • 定制hash表接口,key是threadLocal对象实例,为弱引用,当不再被外部强引用后,可以被垃圾回收。避免 ThreadLocal 本身无法被回收导致的内存泄漏。

2. 导致内存泄漏的原因

2.1 弱引用与强引用

ThreadLocalMap 里存储的键是 ThreadLocal 对象的弱引用,而值是强引用。当外部对 ThreadLocal 对象的强引用被移除之后,由于 ThreadLocal键是弱引用,在下一次垃圾回收时,这个 ThreadLocal键就会被回收掉,变成 null。不过,值依然是强引用,只要线程还在运行,ThreadLocalMap就不会被回收,那么这些键为null对应的值就没办法被访问到,却依旧占用着内存,最终造成内存泄漏。

引用类型简介

在 Java 里,有强引用、软引用、弱引用和虚引用这几种引用类型。其中和 ThreadLocal 内存泄漏问题相关的是强引用和弱引用:

  • 强引用:平时最常用的引用方式,例如 Object obj = new Object();,只要强引用存在,对象就不会被垃圾回收。
  • 弱引用:使用 WeakReference 类来表示,被弱引用关联的对象,在垃圾回收时,无论当前内存是否充足,都会被回收。

ThreadLocal 内存结构

Thread 类中有一个 ThreadLocalMap 类型的成员变量 threadLocals,ThreadLocalMap 是 ThreadLocal 的静态内部类,它以 ThreadLocal 对象为键,存储用户自定义的值。ThreadLocalMap 里的键是对 ThreadLocal 对象的弱引用。

内存泄漏产生过程

当代码中对 ThreadLocal 对象的强引用被移除后,由于 ThreadLocalMap 中的键是弱引用,在下次垃圾回收时,这个 ThreadLocal 键就会被回收,变成 null。 但是 ThreadLocalMap 中的值是强引用,只要线程还在运行,ThreadLocalMap 就不会被回收,那么这些键为 null 对应的值就无法被访问到,却依旧占用着内存,最终造成内存泄漏。

2.2 线程复用问题

在使用线程池的时候,线程会被复用。若 ThreadLocal 在使用完之后没有进行清理,当线程被下一次复用,之前的 ThreadLocal 变量依旧存在,可能会造成数据混乱,同时也会持续占用内存。

以上述的代码为例:

2.3 避免内存泄漏的方法

2.3.1 及时调用remove方法

java 复制代码
public class ConcurrentTest {
    private static final ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = (int) (Math.random() * 100); 
            threadLocalVar.set(value); 
            System.out.println(Thread.currentThread().getName() + " 设置的值:" + threadLocalVar.get());
            // 及时移除 ThreadLocal 中的值
            threadLocalVar.remove(); 
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();
    }
}

2.3.2 使用try-finally块进行remove

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentTest {
    // 创建一个 ThreadLocal 变量
    private static final ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建一个固定大小为 1 的线程池,保证线程复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        // 定义任务
        Runnable task = () -> {
            try {
                // 第一次执行任务时设置值
                if (threadLocalVar.get() == null) {
                    int value = (int) (Math.random() * 100);
                    threadLocalVar.set(value);
                    System.out.println(Thread.currentThread().getName() + " 首次设置的值:" + threadLocalVar.get());
                } else {
                    // 复用线程时获取之前设置的值
                    System.out.println(Thread.currentThread().getName() + " 复用线程获取的值:" + threadLocalVar.get());
                }
            } finally {
                // 任务结束时清除 ThreadLocal 变量的值
                threadLocalVar.remove();
            }
        };

        // 提交任务到线程池
        executorService.submit(task);
        try {
            // 等待第一个任务执行完成
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再次提交任务,复用线程
        executorService.submit(task);

        // 关闭线程池
        executorService.shutdown();
    }
}
相关推荐
乾元32 分钟前
实战案例:解析某次真实的“AI vs. AI”攻防演练
运维·人工智能·安全·web安全·机器学习·架构
qq_2562470541 分钟前
Google 账号防封全攻略:从避坑、保号到申诉解封
后端
全栈游侠1 小时前
STM32F103XX 01-存储器和总线架构
stm32·嵌入式硬件·架构
MX_93591 小时前
使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描
java·后端·spring
弹简特1 小时前
【JavaEE05-后端部分】使用idea社区版从零开始创建第一个 SpringBoot 程序
java·spring boot·后端
爬山算法1 小时前
Hibernate(81)如何在数据同步中使用Hibernate?
java·后端·hibernate
Ivanqhz2 小时前
现代异构高性能计算(HPC)集群节点架构
开发语言·人工智能·后端·算法·架构·云计算·边缘计算
Loo国昌2 小时前
【大模型应用开发】第三阶段:深度解析检索增强生成(RAG)原理
人工智能·后端·深度学习·自然语言处理·transformer
DisonTangor2 小时前
智谱开源基于GLM-V编码器-解码器架构的多模态OCR模型——GLM-OCR
架构·开源·ocr
Demon_Hao2 小时前
Spring Boot开启虚拟线程ScopedValue上下文传递
java·spring boot·后端