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();
    }
}
相关推荐
Answer_ism35 分钟前
【SpringMVC】SpringMVC拦截器,统一异常处理,文件上传与下载
java·开发语言·后端·spring·tomcat
Faith_xzc3 小时前
存算分离是否真的有必要?从架构之争到 Doris 实战解析
大数据·数据库·数据仓库·架构·开源
盖世英雄酱581363 小时前
JDK24 它来了,抗量子加密
java·后端
Asthenia04124 小时前
无感刷新的秘密:Access Token 和 Refresh Token 的那些事儿
前端·后端
Asthenia04124 小时前
面试复盘:聊聊epoll的原理、以及其相较select和poll的优势
后端
luckyext4 小时前
SQLServer列转行操作及union all用法
运维·数据库·后端·sql·sqlserver·运维开发·mssql
Asthenia04125 小时前
ES:倒排索引的原理与写入分析
后端
每次的天空5 小时前
Android第五次面试总结(HR面)
android·面试·职场和发展
圈圈编码5 小时前
Spring常用注解汇总
java·后端·spring
云上的阿七6 小时前
无服务器架构将淘汰运维?2025年云计算形态预测
运维·架构·serverless