ThreadLocal原理

参考:https://luming.blog.csdn.net/article/details/141071290?fromshare=blogdetail&sharetype=blogdetail&sharerId=141071290&sharerefer=PC&sharesource=m0_51140831&sharefrom=from_link

一、什么是ThreadLocal

在Java中,ThreadLocal 类提供了一种方式,使得每个线程可以独立地持有自己的变量副本,而不是共享变量。这可以避免线程间的同步问题,因为每个线程只能访问自己的ThreadLocal变量。通过ThreadLocal为线程添加的值只能由这个线程访问到,其他的线程无法访问,因此就避免了多线程之间的同步问题

使用ThreadLocal时,通常需要实现以下步骤:

  • 初始化:创建ThreadLocal变量。
1
private static ThreadLocal<T> threadLocal = new ThreadLocal<>();
  • 设置值:使用set(T value)方法为当前线程设置值。
1
threadLocal.set(value);
  • 获取值:使用get()方法获取当前线程的值。
1
T value = threadLocal.get();
  • 移除值:使用remove()方法在线程结束时清除ThreadLocal变量,以避免内存泄漏。
1
threadLocal.remove();
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
// 设置线程局部变量的值
threadLocal.set(10);

// 这个值在其他线程中是取不到获取的
new Thread(() -> {
Integer value = threadLocal.get();//null
System.out.println("Thread value: " + value);
}).start();
}
}

img

二、ThreadLocal的数据结构

在JDK8之后每一个线程都会维护一个ThreadLocalMap,这个Map是一个哈希散列结构,如下图所示,每一个元素(Entry)都是一个键值对,key为ThreadLocal,Value为存储的数据,也就是set()方法存储的内容。

img

三、ThreadLocal的内存泄露问题

首先是内存泄漏的概念:

  • 内存溢出:没有足够的内存供申请者使用
  • 内存泄漏:程序中已经动态分配的内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至崩溃。此外内存泄漏的堆积最终也会导致内存溢出。

下图是ThreadLocal相关的内存结构图,在栈区中有threadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。
img

那么所谓的内存泄漏,其实就是指的Entry这块内存不能正确释放

强弱引用的概念:

  • 强引用(StrongReference):就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用(WeakReference):垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果Key是强引用

当我们在业务代码中使用完ThreadLocal,在栈区指向堆区的这个指向关系就会被回收掉了,但是由于Key是强引用指向ThreadLocal,故而堆区中的ThreadLocal无法被回收,此时的Key指向ThreadLocal,另外由于当前线程还没有结束,则下面那条强引用指向关系任然存在。故为下图的关系状态
img

在这样的情况下,由于栈上的指向已经消失了,我们无法访问到堆上的ThreadLocal,故而无法访问到Entry,但是Entry又有Map指向它,故而无法进行回收。那么此时的Entry即无法访问也无法回收,这就造成了Entry的内存泄露

如果Key是弱引用

其次是弱引用的情况,当我们在业务代码中使用完ThreadLocal就通过垃圾回收(GC)进行了回收,那么由于Key是弱引用,Key此时就指向null,但是由于当前线程还没有结束,则下面那条强引用指向关系仍然存在

img

在这样的情况下,Entry由于仍然有Map指向它所以不会被GC回收掉,但是此时的Key又为null,所以我们无法访问到这个Value。这就导致了这个Value我们即不能访问到也不能进行回收,此时就造成了Value的内存泄漏

总结

通过以上分析,我们得知了不管Entry中的Key是否为弱引用,都会造成内存泄漏的情况,只不过强引用下是Entry的内存泄漏,弱引用下是Value的内存泄漏。造成这样内存泄漏的情况都有这样的共同特性:

  • 都没有手动删除Entry
  • 当前线程都在运行

也就是说,只要我们在使用完ThreadLocal后,调用其remove()方法删除对应的Entry就可以避免内存泄漏的问题。

并且由于ThreadLoaclMap是Thread的一个属性,故而它的生命周期和线程一样,那么当线程的生命周期结束,自然也就没有Map指向Entry,这也就在根源上解决了问题。

综上所述,造成ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的Key就会导致内存泄漏


ThreadLocal原理
http://example.com/2025/05/13/ThreadLocal原理/
作者
Kon4tsu
发布于
2025年5月13日
许可协议