ThreadLocal原理以及内存泄漏

1.ThreadLoacal是干什么的?

ThreadLocal是用来实现线程隔离的数据存储。 它能让每个线程独立持有一份变量副本,互不干扰。

ThreadLocal = 每个线程都有自己的“私有小仓库”。

示例:


ThreadLocal<String> local = new ThreadLocal<>(); new Thread(() -> { local.set("A线程的数据"); System.out.println(local.get()); }).start(); new Thread(() -> { local.set("B线程的数据"); System.out.println(local.get()); }).start();

输出:


A线程的数据 B线程的数据

两条线程各用各的,不会串。

2.ThreadLocal的底层原理

ThreadLocal提供了一种线程内独享的变量机制,使每个线程都能有自己独立的变量副本。每个线程内部维护一个ThreadLocalMap!!!这个ThreadLocalMap用于存储线程独立的变量副本。ThreadLocalMapThreadLocal实例作为key,以线程独立的变量副本作为值。不同的线程通过ThreadLocal获取各自的变量副本,而不会影响其他线程的数据。

设计原理

误区:

不是将ThreadLocal看作一个map,然后每个线程都是key,这样每个线程去调用ThreadLocal.get的时候,将自身作为key去map中找,获取各自的value。这个是错误的!!!这样ThreadLocal就变成了共享变量了。多个线程竞争ThreadLocal,又得加锁,又回到原点。

需要在每个线程的本地都存一份值,每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,这就需要用到map。就是刚才讲的。

比如:现在有三个ThreadLocal对象,两个线程


ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();

那么他们对应的关系就是:

这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求。

底层源码

Thread对象里边会有一个ThreadLocalMap,用来保存本地变量。

源码:


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

但是这个map是ThreadLocal的静态内部类,这个变量名称是threadLocals

ThreadLocalMap 是 ThreadLocal 的静态内部类,是为了表达逻辑归属关系,但又不让 Map 对 ThreadLocal 实例形成强引用,从而避免内存泄漏。

ThreadLocalMap的定义:

里边有一个Entity数组,继承了WeakReference弱引用。但是这个不是说是数组弱引用,而是Entry里边的super(k),这个key才是弱引用。

所以ThreadLocalMap里边有个数组,数组的ket就是ThreadLocal对象, value就是我们要保存的值。

ThreadLocal.get()方法

从这可以看出为什么不同的线程对同一个ThreadLocal对象调用get方法能得到不同的值。

map.getEntry(this)

key是如何从ThreadLocalMap中找到Entry的。

3.内存泄漏

什么是内存泄漏?

内存泄漏是指:程序中不再使用的对象仍然被引用着,导致垃圾回收器(GC)无法释放它们的内存空间。

示例:静态集合导致的内存泄漏


public class MemoryLeakDemo { // 静态集合(全局存在,不会被 GC 回收) private static List<byte[]> memoryHolder = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { // 每次添加 1MB 数据 byte[] data = new byte[1024 * 1024]; memoryHolder.add(data); System.out.println("第 " + (i + 1) + " 次添加, 当前集合大小: " + memoryHolder.size()); Thread.sleep(100); // 稍微等一下,方便观察 } } }

  1. memoryHolder 是 静态变量,属于类 MemoryLeakDemo,会一直存在于 JVM 方法区中。
    即使 main() 结束,它也不会被销毁。
  2. 每次循环创建 1MB 的 byte[] 数组,然后存进这个集合。
    这些数组都被强引用着 (ArrayList 里的引用)。
  3. 垃圾回收器无法清理这些数组,因为它们仍被 memoryHolder 引用着。
  4. 运行一会后你会看到:OOM

java.lang.OutOfMemoryError: Java heap space

只要释放引用,GC 就能正常工作: memoryHolder.clear(); // 清空集合

又比如再ThreadLocal发生的内存溢出问题


ThreadLocal<byte[]> local = new ThreadLocal<>(); local.set(new byte[1024 * 1024]); // 1MB 数据 local = null; // ThreadLocal对象没了 // 但 ThreadLocalMap 还在引用 value → 内存泄漏

调用threadLocal.remove();就不会发生这个问题。

ThreadLocal为什么会发生内存泄漏呢?

刚才已经讲明了

key 是弱引用(WeakReference)
value 是强引用(Object)

当我们执行


ThreadLocal<User> local = new ThreadLocal<>(); local.set(new User("xxx"));

底层会变成:

存放位置内容
当前线程的 ThreadLocalMapkey = ThreadLocal(弱引用)
value = User(强引用)

如果稍后你把: local = null;

那此时:

  • 这个 ThreadLocal 对象在外部没有强引用
  • 因为它是弱引用 → 下一次 GC 会回收它(key 变成 null
  • 但 value(User 对象)仍然被强引用着(Map 内部)!

这就导致:

ThreadLocalMap 里有个 “key=null、value=仍然存在” 的 Entry,value 永远不会被释放

那你可能会说,把里边的value也变成弱引用不就好了

× 不行, 如果 value 是弱引用,那么只要发生 GC,value 可能被清理掉。然后你下一次 get() 时,就拿不到数据了,线程内部状态会丢失。

举个例子 :


ThreadLocal<User> local = new ThreadLocal<>(); local.set(new User("xxx")); // 这时 value = new User("xxx") // 如果 value 是 WeakReference,GC 一跑,它就被清了 System.gc(); System.out.println(local.get()); // null

这就破坏了 ThreadLocal 的核心语义:“每个线程拥有独立的一份数据,直到手动清除或线程结束前都能访问”。

那 static ThreadLocal 呢


private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();

优点:

  • key 不会被 GC,避免 “key=null” 的 Entry;
  • 每次请求都可以覆盖旧值,不容易泄漏。

但问题是:

问题原因
value 仍然留在线程中如果线程池复用线程,而你没调用 remove(),老的 value 仍然在
static 生命周期过长整个应用期间一直存在,可能造成全局数据滞留
大对象浪费内存即使不访问,也常驻内存(尤其在线程池中)

所以它确实能缓解泄漏问题,但不能彻底解决

为什么线程池中更危险?

线程池线程不会销毁!
每个线程都有自己的 ThreadLocalMap

如果你使用了 ThreadLocal,但没有及时 remove()

  • key 被回收(key=null)
  • value 还在
  • 线程长期存在(被线程池复用)
  • 这些“脏 value” 永远留在线程中 → 内存持续上涨 → OOM

所以线程池 + ThreadLocal 是高危组合

ThreadLocalMap 的“清理机制”

ThreadLocalMap 的设计者(Doug Lea)意识到这个风险,
所以在源码中加入了“懒惰清理” 逻辑:

每次调用 get()set()remove() 时,会顺带清理掉 key=null 的 Entry。


private void expungeStaleEntry(int i) { table[i].value = null; table[i] = null; }

但注意:

如果你的代码永远不再访问这个 ThreadLocal(比如请求结束后),那清理永远不会触发!

所以这不是自动回收机制,只是懒清理

正确用法

关键点就是 —— 用完要 remove()


private static final ThreadLocal<User> THREAD_LOCAL = new ThreadLocal<>(); public void handleRequest() { try { THREAD_LOCAL.set(new User("xxx")); // ...业务逻辑 } finally { THREAD_LOCAL.remove(); // ✅ 防止内存泄漏 } }

推荐写在 finally 块中,无论发生什么都能执行。

总结

ThreadLocal 泄漏的本质不是 ThreadLocal 自身的问题,而是 ThreadLocalMap 的 value 没有被释放,Thread 又长期存在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值