文章目录
- [一、ThreadLocal 基本概念](#一、ThreadLocal 基本概念)
- [二、ThreadLocal 的数据结构](#二、ThreadLocal 的数据结构)
- [三、GC 之后 ThreadLocal 的 key 是否为 null?](#三、GC 之后 ThreadLocal 的 key 是否为 null?)
-
- [3.1 Java 的四种引用类型](#3.1 Java 的四种引用类型)
- [3.2 ThreadLocal 的 key 是弱引用](#3.2 ThreadLocal 的 key 是弱引用)
- [3.3 代码演示:GC 后 key 的状态](#3.3 代码演示:GC 后 key 的状态)
- [3.4 关键点分析](#3.4 关键点分析)
- [3.5 内存泄漏问题](#3.5 内存泄漏问题)
- [四、ThreadLocal.set() 方法源码详解](#四、ThreadLocal.set() 方法源码详解)
- [五、ThreadLocalMap 的 Hash 算法](#五、ThreadLocalMap 的 Hash 算法)
- [六、ThreadLocalMap 的 Hash 冲突](#六、ThreadLocalMap 的 Hash 冲突)
- 七、ThreadLocalMap.set()详解
一、ThreadLocal 基本概念
ThreadLocal 提供了线程局部变量的功能,即每个线程都能有一份自己的副本,这样不同线程之间的数据是互不干扰的。
线程每次访问 ThreadLocal 变量时,都会获取到自己独立的值。它的主要用途是解决多线程环境下共享数据的问题,避免了同步带来的开销。
代码示例:
java
public class ThreadLocalTest {
private List<String> messages = new ArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("hello world!");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}
输出 :

代码解析:
- ThreadLocal 初始化 :
holder是一个ThreadLocal对象,它通过ThreadLocal.withInitial(ThreadLocalTest::new)初始化。这意味着每个线程首次调用holder.get()时,都会创建一个新的ThreadLocalTest实例。- add 方法 :
add方法通过holder.get()获取当前线程的ThreadLocalTest实例,并向其messages列表中添加消息。- clear 方法 :
clear方法首先获取当前线程的messages列表,然后调用holder.remove()清除当前线程的ThreadLocal变量。最后,它打印出holder.get().messages.size(),由于holder已经被清除,所以会重新初始化一个新的ThreadLocalTest实例,因此messages列表的大小为 0。
二、ThreadLocal 的数据结构

1. ThreadLocalMap :每个 Thread 对象都有一个 ThreadLocal.ThreadLocalMap 类型的实例变量 threadLocals。这个 ThreadLocalMap 是 ThreadLocal 的内部类,用于存储线程局部变量。
2. ThreadLocalMap 的实现:
ThreadLocalMap的实现类似于HashMap,但它没有链表结构。它使用一个数组来存储键值对,其中键是ThreadLocal对象,值是线程局部变量。ThreadLocalMap中的Entry类继承自WeakReference<ThreadLocal<?>>,这意味着Entry的键(即ThreadLocal对象)是弱引用 。这有助于在ThreadLocal对象不再被使用时,垃圾回收器可以回收它,从而避免内存泄漏。
3. 线程隔离 :每个线程在往 ThreadLocal 里放值的时候,都会往自己的 ThreadLocalMap 里存。读取时也是以 ThreadLocal 作为引用,在自己的 map 里找对应的键,从而实现了线程隔离。
三、GC 之后 ThreadLocal 的 key 是否为 null?
为了回答这个问题,我们需要深入理解 Java 的引用类型以及 ThreadLocal 的内部实现机制。
3.1 Java 的四种引用类型
- 强引用 :
- 最常见的引用类型,例如
Object obj = new Object()。 - 只要强引用存在,垃圾回收器永远不会回收被引用的对象,即使内存不足时。
- 最常见的引用类型,例如
- 软引用 :
- 使用
SoftReference修饰的对象。 - 在内存即将溢出时,垃圾回收器会回收软引用指向的对象。
- 使用
- 弱引用 :
- 使用
WeakReference修饰的对象。 - 只要发生垃圾回收,若对象只被弱引用指向,就会被回收。
- 使用
- 虚引用 :
- 使用
PhantomReference修饰的对象。 - 虚引用的唯一作用是接收对象即将被回收的通知。
- 使用
3.2 ThreadLocal 的 key 是弱引用
ThreadLocal 的 key 是弱引用类型(WeakReference<ThreadLocal<?>>)。这意味着:
- 如果
ThreadLocal对象没有被强引用指向,垃圾回收器会回收该对象。 - 如果
ThreadLocal对象仍然被强引用指向,垃圾回收器不会回收它。
3.3 代码演示:GC 后 key 的状态
以下代码通过反射检查 GC 后 ThreadLocal 中 key 的状态:
java
public class ThreadLocalDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object ThreadLocalMap = field.get(t);
Class<?> tlmClass = ThreadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:


如图所示,因为这里创建的ThreadLocal并没有指向任何值,也就是没有任何引用:
java
new ThreadLocal<>().set(s);
所以这里在GC之后,key就会被回收,我们看到上面debug中的referent=null, 如果改动一下代码:

3.4 关键点分析

- 为什么 key 会被回收?
- 代码中创建的
ThreadLocal对象是局部变量,没有强引用指向它。 - 当触发
GC时,ThreadLocal对象只被弱引用指向,因此被回收,key变为null。
- 代码中创建的
- 为什么值未被回收?
ThreadLocalMap的Entry中,key是弱引用,但value是强引用。- 如果
key被回收,value仍然存在,导致内存泄漏。
ThreadLocal.get()时key是否为null?- 如果
ThreadLocal对象仍然被强引用指向,key不会为null。 - 如果
ThreadLocal对象没有被强引用指向,key可能被回收,变为null。
- 如果
3.5 内存泄漏问题
- 原因 :
ThreadLocalMap的Entry中,key是弱引用,value是强引用。
如果key被回收,value仍然存在,且无法通过ThreadLocal访问,导致内存泄漏。 - 解决方案:使用完
ThreadLocal后,调用remove()方法清除Entry,避免内存泄漏。
四、ThreadLocal.set() 方法源码详解
ThreadLocal 中的 set 方法用于将当前线程的局部变量设置为指定的值。其核心逻辑是判断 ThreadLocalMap 是否存在,然后根据情况进行相应的处理。下面我们逐步解析 set 方法的源码及其背后的原理。
1. set 方法源码
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocal中的set方法原理如上图所示,很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocal中的set方法进行数据处理。
五、ThreadLocalMap 的 Hash 算法
1. 基本原理
ThreadLocalMap 作为一种 Map 结构,需要实现自己的 hash 算法来解决散列表数组冲突问题。其 hash 算法核心代码为:
java
int i = key.threadLocalHashCode & (len - 1);
其中,
i就是当前key在散列表中对应的数组下标位置。
2. threadLocalHashCode 值的计算
threadLocalHashCode 值的计算在 ThreadLocal 类中完成,相关代码如下:
java
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
每当创建一个
ThreadLocal对象,ThreadLocal.nextHashCode这个值就会增长0x61c88647。这个值是斐波那契数(黄金分割数),使用它作为 hash 增量,能让 hash 分布非常均匀。
六、ThreadLocalMap 的 Hash 冲突
注明: 下面所有示例图中,绿色块Entry代表正常数据,灰色块代表Entry的key值为null,已被垃圾回收。白色块表示Entry为null。
1. 不同的解决方式
- HashMap:使用数组 + 链表(或红黑树)的方式解决冲突,冲突的数据挂载到链表上,当链表长度超过一定数量则会转化成红黑树。
- ThreadLocalMap :没有链表结构,不能采用 HashMap 解决冲突的方式。
2. 线性探测法
当插入数据时,如果通过 hash 计算得到的槽位已经有 Entry 数据,就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,并将当前元素放入此槽位中。

如上图所示,如果我们插入一个value=27的数据,通过hash计算后应该落入第4个槽位中,而槽位4已经有了Entry数据。此时就会线性向后查找,一直找到Entry为null的槽位才会停止查找,将当前元素放入此槽位中。
3. 特殊情况处理
在迭代过程中,会遇到不同情况,如 Entry 不为 null 且 key 值相等的情况,还有 Entry 中的 key 值为 null 的情况等,会有不同的处理方式。由于 Entry 中的 key 是弱引用类型,可能会存在 key 过期的 Entry 数据。在 set 过程中,如果遇到了 key 过期的 Entry 数据,实际上会进行一轮探测式清理操作。
七、ThreadLocalMap.set()详解
看完了ThreadLocal hash算法后,我们再来看set是如何实现的。
往ThreadLocalMap中set数据(新增或者更新数据)分为好几种情况,针对不同的情况我们画图来说说明。
第一种情况: 通过hash计算后的槽位对应的Entry数据为空:

直接将数据放到该槽位即可
第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:

直接更新该槽位的数据。
第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry:

遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key值相等的数据,直接更新即可。
第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,一到了index=7的槽位数据Entry的key=null:

散列数组下标为7位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了,此时就会执行
replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。
具体步骤如下:
1. 初始化探测式清理过期数据扫描的开始位置
设置 slotToExpunge = staleSlot = 过期槽位的下标。例如,当遇到 index = 7 的槽位数据 Entry 的 key = null 时,staleSlot = 7,slotToExpunge 初始也为 7。
2. 以当前 staleSlot 开始向前迭代查找
通过 for 循环迭代,从 staleSlot 位置向前查找其他过期的数据,并更新过期数据起始扫描下标 slotToExpunge,直到碰到 Entry 为 null 时结束迭代。例如,在向前迭代过程中,如果又找到了其他过期的数据,会继续向前查找,直到遇到 Entry = null 的槽位才停止,此时 slotToExpunge 会被更新为新的过期元素的下标。这个操作的目的是为了确定当前过期槽位 staleSlot 之前是否还有过期元素。
如下图所示,slotToExpunge被更新为0:

3. 接着以 staleSlot 位置向后迭代
-
若找到了相同 key 值的 Entry 数据:从当前节点 staleSlot 向后查找 key 值相等的 Entry 元素,找到后更新该 Entry 的值,并交换 staleSlot 元素的位置(因为 staleSlot 位置为过期元素),然后开始进行过期 Entry 的清理工作。

-
若没有找到相同 key 值的 Entry 数据:从当前节点 staleSlot 向后查找 key 值相等的 Entry 元素,直到遇到 Entry 为 null 则停止寻找。若此时 table 中没有 key 值相同的 Entry,则创建新的 Entry,替换 table[staleSlot] 位置的数据。


4. 过期元素清理工作
无论是更新 Entry 后还是替换 Entry 后,都会进行过期元素清理工作。清理工作主要涉及两个方法:expungeStaleEntry() 和 cleanSomeSlots(),具体细节后续会进一步讲解。
