专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
笔吧评测室  ·  微星泰坦、雷影、绝影系列 2025 ... ·  昨天  
笔吧评测室  ·  机械革命耀世 16 Ultra ... ·  昨天  
笔吧评测室  ·  联想 YOGA Air 14 Aura ... ·  2 天前  
笔吧评测室  ·  雷蛇灵刃 16/18 2025 ... ·  2 天前  
笔吧评测室  ·  聊一款「屏幕超亮,性能激进」的轻薄本 ·  2 天前  
51好读  ›  专栏  ›  Java基基

面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?

Java基基  · 公众号  ·  · 2025-02-26 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7395024434681937971


介绍

ThreadLocal 中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

使用

在使用 ThreadLocal 时,可以直接通过 set(T value) get() 来设置 threadLocal 的值、获取 threadLocal 的值。

set 方法

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
    if (map != null) { // 如果map不是空
        map.set(this, value); // 设置值
    } else {
        createMap(t, value); // 创建并设置值
    }
}

// 获取线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 对该ThreadLocal设置值
private void set(ThreadLocal> key, Object value) {

    // ThreadLocalMap内部的table数组
    Entry[] tab = table;
    int len = tab.length;
    // 根据threadLocal的hash和长度进行与运算,找到下标位置
    int i = key.threadLocalHashCode & (len-1);

    // 曾经该threadLocal有值,设置值并返回
    for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取entry的引用
        ThreadLocal> k = e.get();
        // 引用等于当前threadLocal 则进行设置值
        if (k == key) {
            e.value = value;
            return;
        }
        // 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 组装entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

其中 threshold = len * 2 / 3,它是通过 setThreshold 方法进行设置的。而每次 rehash 的时候都会调用 resize 方法,它会读取 oldTable.length,把 newLen 设置为 oldLen 的两倍。

这里有一个注意点 int i = key.threadLocalHashCode & (len-1); 下标是通过 hash 来确定的,会出现 hash 冲突,这里采用的是开放地址法来解决 hash 冲突,在下面的代码中有判断 k==key ,如果不相等则 nextIndex(i, len) 获取下一个下标来判断。

上述就是整个 set 的过程,下面来看一下 get

public T get() {
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // this为当前threadLocal,获取对应的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 返回当前entry的值即可。
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 设置初始值并返回,初始值是null
    return setInitialValue();
}


private Entry getEntry(ThreadLocal> key) {
    // 查找下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 找到对应entry进行返回
        return e;
    else
        // 开始遍历entry数组,如果能找到key的entry就返回否则返回null
        return getEntryAfterMiss(key, i, e);
}

get 方法要比 set 简单很多,只是根据 key 找对应 entry,把 entry 的值返回即可。

结构

通过上述源码,可以总结出 threadLocal 的数据结构如下:

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

问题

根据上面的介绍,可以看出一些潜在的问题;例如在使用 threadLocal 时堆栈信息如下:

真的会内存泄漏?

当使用完 threadLocal,threadLocal 的对象引用就不存在了,而 key 对 threadLocal 是弱引用,gc 后这段引用也不存在了。此时无法通过 map.getEntry(this) 找到对应的 entry,而 entry 还一直存在 Entry[]中,就有 可能 导致了内存溢出。这里我写了是有可能导致内存溢出,例如在 set 方法中有这样一行代码

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

该方法的具体代码如下:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

当有新的 threadlocal 进行设置值时都会进行清除一下 e.get() == null 引用为空的 Entry,而进入到这里的条件是 (n >>>= 1) != 0 ,当长度为 16(10000)会触发 5 次,挨着当前 threadlocal 的 Entry 的连续 5 个都没有引用为 null 的话,就不会继续往下移除了。

所以如果频繁的调用 set 方法,它也会帮助清除一些之前 key 已经被 gc 掉的 entry 对象,但无论如何如果没有 gc 和调用 set 方法的话,这些 entry 对象会一直在内存中占用。

所以每次在使用完 threadlocal 时要调用一下 remove 方法,它会自动把 entry 移除。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}

除此之外在 threadlocal 时,尽量把它设置为 pricate static 变量,这样因为 threadLocal 的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到 Entry,并对其进行 remove

Entry 的 key 设置为强引用可以么?

当 ThreadLocal 的引用在用户栈中已经移除了,并且没有调用 remove 方法;但是 entry 还有一个强引用指向 threadLocal 对象, e.get() 永远都不会是空,此时 entry 对象就永远无法被回收掉了。







请到「今天看啥」查看全文