文章目录
- [一、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()
,具体细节后续会进一步讲解。
