ThreadLocalMap
文章目录
- ThreadLocalMap
-
- [1. `ThreadLocal` 的基础结构](#1.
ThreadLocal
的基础结构) -
- [1.1 类继承关系](#1.1 类继承关系)
- [1.2 `ThreadLocalMap` 内部类概述](#1.2
ThreadLocalMap
内部类概述) - [1.3 弱引用键的设计理念](#1.3 弱引用键的设计理念)
- [2. `ThreadLocalMap`的实现机制](#2.
ThreadLocalMap
的实现机制) -
- [2.1 Entry结构与弱引用](#2.1 Entry结构与弱引用)
- [2.2 哈希表设计](#2.2 哈希表设计)
- [2.3 解决哈希冲突的策略](#2.3 解决哈希冲突的策略)
- [3. `ThreadLocalMap`的关键操作源码分析](#3.
ThreadLocalMap
的关键操作源码分析) -
- [3.1 set方法实现](#3.1 set方法实现)
- [3.2 get方法实现](#3.2 get方法实现)
- [3.3 remove方法实现](#3.3 remove方法实现)
- [3.4 过期Entry的清理机制](#3.4 过期Entry的清理机制)
- [4. 核心操作源码分析](#4. 核心操作源码分析)
-
- [4.1 `ThreadLocal`.set方法实现](#4.1
ThreadLocal
.set方法实现) - [4.2 `ThreadLocal.get`方法实现](#4.2
ThreadLocal.get
方法实现) - [4.3 `ThreadLocal.remove`方法实现](#4.3
ThreadLocal.remove
方法实现) - [4.4 `ThreadLocal.withInitial`方法](#4.4
ThreadLocal.withInitial
方法)
- [4.1 `ThreadLocal`.set方法实现](#4.1
- [5. 内存泄漏问题深度剖析](#5. 内存泄漏问题深度剖析)
-
- [5.1 为什么会发生内存泄漏](#5.1 为什么会发生内存泄漏)
- [5.3 源码中的防泄漏设计](#5.3 源码中的防泄漏设计)
- [5.4 清理机制的触发时机](#5.4 清理机制的触发时机)
- [1. `ThreadLocal` 的基础结构](#1.
你是否曾遇到这样的场景:一个对象需要在同一个线程内的多个方法中传递,却不想把它作为参数在每个方法间传来传去?或者你需要保存一个"线程上下文"信息,比如用户身份、事务ID或请求追踪信息?

在并发的世界里,全局变量是危险的。正如一个热闹的宿舍里,把你的私人物品放在公共区域,总会有人"不小心"把它们挪走。ThreadLocal
就像是给每个线程发了一个专属保险柜,只有线程自己能存取,线程间互不干扰。
然而,这个看似简单的工具类背后隐藏着巧妙的设计和潜在的风险。Java面试中,ThreadLocal
的原理和内存泄漏问题"几乎成了高频标配题。许多开发者对ThreadLocal
的印象仅限于"每个线程拥有独立的变量副本",但对其内存模型、实现机制和正确使用姿势知之甚少。
1. ThreadLocal
的基础结构
1.1 类继承关系
我们来看一下ThreadLocal
的类定义:
java
public class ThreadLocal<T> {
// 实现代码...
}
ThreadLocal
是一个泛型类,类型参数T表示它可以存储任何类型的对象。相比其他并发工具类,ThreadLocal
的类结构非常简单,它没有继承任何类,也没有实现任何接口。
这种设计反映了它的专注性------只做一件事:为每个线程提供独立的变量副本。
1.2 ThreadLocalMap
内部类概述
ThreadLocal
最核心的部分是其内部私有静态类ThreadLocalMap
:
java
static class ThreadLocalMap {
/**
* Entry继承自WeakReference,key是ThreadLocal对象的弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 与这个ThreadLocal关联的值 */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/** 初始容量 - 必须是2的幂 */
private static final int INITIAL_CAPACITY = 16;
/** 存放数据的table,大小必须是2的幂 */
private Entry[] table;
/** table中的entry数量 */
private int size = 0;
/** 扩容阈值,默认为0 */
private int threshold = 0;
// 其他字段和方法...
}
乍一看,这个ThreadLocalMap
结构很像HashMap
,都是基于哈希表实现的。但有几个关键区别:
-
ThreadLocalMap
是ThreadLocal
的静态内部类,但它的实例是保存在Thread类中的 -
Entry的key是
ThreadLocal
对象的弱引用,这对内存管理非常重要 -
ThreadLocalMap
没有实现Map接口,是一个定制的哈希表
1.3 弱引用键的设计理念
ThreadLocalMap
中使用弱引用来保存ThreadLocal
对象,这是一个精妙的设计:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 将ThreadLocal对象作为弱引用
value = v;
}
}
为什么要用弱引用?想象这样一个场景:
java
void someMethod() {
ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(new User("John"));
// 使用userThreadLocal
// 方法结束,userThreadLocal变量不再可访问
}
当方法执行完毕,局部变量userThreadLocal就会被销毁,但是Thread对象中的ThreadLocalMap
仍然持有对这个ThreadLocal
的引用。如果是强引用,那么即使userThreadLocal变量已经不可访问,ThreadLocal
对象也不会被垃圾回收,这可能导致内存泄漏。
通过使用弱引用,一旦外部不再持有ThreadLocal
的强引用,垃圾回收器就可以回收这个ThreadLocal
对象,即使它仍被ThreadLocalMap
引用。
但是,此时Entry中的value还是强引用,不会被自动回收,这就是常说的ThreadLocal
内存泄漏的根源。后面我们会详细讨论这个问题。
2. ThreadLocalMap
的实现机制
2.1 Entry结构与弱引用
ThreadLocalMap
内部使用Entry数组来存储数据:
java
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
每个Entry包含:
- 一个
ThreadLocal
的弱引用作为key - 一个普通的强引用Object作为value
值得注意的是,WeakReference是JDK提供的一种特殊引用类型,当一个对象只剩下弱引用指向它时,垃圾回收器会在下一次GC时回收该对象。
java
// 弱引用的基本使用
ThreadLocal<?> threadLocal = new ThreadLocal<>();
WeakReference<ThreadLocal<?>> weakRef = new WeakReference<>(threadLocal);
// 如果此时threadLocal = null,那么weakRef.get()在下一次GC后可能返回null
threadLocal = null;
System.gc(); // 触发GC
ThreadLocal<?> referent = weakRef.get(); // 可能已经是null
这种设计让ThreadLocal
实例在不再需要时可以被回收,同时Entry中的value依然存在,直到Thread结束或者手动调用remove()方法。
2.2 哈希表设计
ThreadLocalMap
使用开放地址法解决哈希冲突:
java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
与HashMap
使用链表或红黑树解决冲突不同,ThreadLocalMap
使用线性探测法:如果计算出的槽位已被占用,就继续往后查找,直到找到空槽位或找到目标Entry。
ThreadLocalMap
中每个ThreadLocal
的哈希值通过一个特殊的原子递增生成器计算:
java
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);
}
这里使用的增量0x61c88647是一个神奇的数字,它是斐波那契散列乘数,能够使哈希值均匀分布,减少冲突。
2.3 解决哈希冲突的策略
当发生哈希冲突时,ThreadLocalMap
采用线性探测法来解决:
java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这段代码展示了ThreadLocalMap
如何处理冲突:
-
计算初始槽位i
-
如果该位置已被占用,检查key是否相同
- 如果key相同,更新value并返回
- 如果key为null(已被GC回收),替换这个"陈旧"的Entry
- 否则,继续查找下一个位置
-
如果找到空槽位,插入新Entry
-
增加size并检查是否需要清理或扩容
值得注意的是,在查找过程中如果发现key为null的Entry(表示ThreadLocal
已被回收),会主动清理这些Entry,这是一种"顺手清理"的机制,有助于减少内存泄漏。
3. ThreadLocalMap
的关键操作源码分析
3.1 set方法实现
set方法是ThreadLocalMap
最核心的方法之一,用于设置键值对:
java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这段代码处理了三种情况:
-
找到相同的key,更新value
-
找到key为null的"陈旧"Entry,替换它
-
找到空槽位,插入新Entry
其中第2点很重要,这是ThreadLocalMap
自动清理机制的一部分。当检测到key为null的Entry时,调用replaceStaleEntry方法替换并清理:
java
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前扫描,查找更多的陈旧Entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) {
if (e.get() == null)
slotToExpunge = i;
}
// 向后遍历
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到key,与staleSlot位置的Entry交换
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果在前面的扫描中没有找到陈旧条目,则起点为这里
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理陈旧的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果我们还没有找到staleSlot之前的陈旧条目
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果我们没有找到key,把新的Entry放入staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果有其他的陈旧Entry,清理它们
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
这个方法相当复杂,它不仅替换了陈旧的Entry,还会主动清理其他陈旧Entry,以减少内存泄漏的可能性。
3.2 get方法实现
get方法用于获取与ThreadLocal
关联的值:
java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
get方法首先计算哈希索引,然后检查该位置的Entry。如果key不匹配或为null,则调用getEntryAfterMiss继续查找。
同样,在查找过程中如果发现key为null的Entry,会调用expungeStaleEntry进行清理,这是另一个"顺手清理"的时机。
3.3 remove方法实现
remove方法用于移除ThreadLocal
关联的值:
java
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); // 清除引用
expungeStaleEntry(i); // 清理这个槽位及后续陈旧的Entry
return;
}
}
}
remove方法首先查找key对应的Entry,找到后调用Entry.clear()方法清除ThreadLocal
的弱引用,然后调用expungeStaleEntry清理这个槽位及后续可能的陈旧Entry。
这个方法是彻底清除ThreadLocal
数据的正确方式,它不仅移除了value的强引用,还处理了可能的连锁效应。
3.4 过期Entry的清理机制
ThreadLocalMap
内部实现了多种清理机制来处理过期(陈旧)的Entry:
java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除指定槽位的Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新哈希后续的Entry
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 如果需要,重新放置Entry到正确位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
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;
}
private void rehash() {
expungeStaleEntries(); // 完全清理所有陈旧Entry
// 如果大小仍然超过阈值的3/4,则扩容
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
ThreadLocalMap
提供了多个级别的清理机制:
-
定点清理:expungeStaleEntry方法清理指定位置的陈旧Entry,并重新哈希后续Entry
-
探测式清理:cleanSomeSlots方法在有限步数内查找并清理陈旧Entry
-
全量清理:expungeStaleEntries方法扫描整个表,清理所有陈旧Entry
这些清理机制在不同操作中被触发:
- set操作可能触发探测式清理和rehash
- get操作在查找过程中可能触发定点清理
- remove操作总是触发定点清理
- 当size达到阈值时,会触发全量清理和可能的扩容
通过这些多层次的清理机制,ThreadLocalMap
尽量减少内存泄漏的可能性,但并不能完全避免。
4. 核心操作源码分析
4.1 ThreadLocal
.set方法实现
ThreadLocal
的set方法是我们直接使用的API
,看看它是如何实现的:
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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个方法非常简洁:
-
获取当前线程对象
-
尝试获取线程中的
ThreadLocalMap
-
如果map存在,调用map.set设置值
-
如果map不存在,创建一个新的
ThreadLocalMap
关键点在于:ThreadLocalMap
实例是存储在Thread对象中的,而不是ThreadLocal
对象中。每个Thread都有自己的ThreadLocalMap
,用于存储所有与该线程关联的ThreadLocal
变量。
这就是ThreadLocal
能够实现线程隔离的核心机制------数据实际上是存储在线程内部的。
4.2 ThreadLocal.get
方法实现
get方法用于获取当前线程下的变量值:
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
get方法的逻辑是:
-
获取当前线程的
ThreadLocalMap
-
如果map存在,尝试获取this作为key的Entry
-
如果Entry存在,返回其value
-
否则,调用setInitialValue设置并返回初始值
initialValue方法默认返回null,但可以被子类重写以提供自定义的初始值。这是一个很有用的特性,比如可以用来定义线程本地变量的默认值。
4.3 ThreadLocal.remove
方法实现
remove方法用于移除当前线程中的变量:
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
这个方法同样简单明了:获取当前线程的ThreadLocalMap
,如果存在则从中移除this对应的Entry。
正确调用remove方法对防止内存泄漏至关重要,尤其是在线程池环境中。线程被重用时,如果不清理ThreadLocal
变量,新的任务可能会访问到之前任务设置的值,既造成了内存泄漏,又可能导致逻辑错误。
4.4 ThreadLocal.withInitial
方法
从Java 8开始,ThreadLocal
增加了一个工厂方法withInitial:
java
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
这个方法创建一个特殊的ThreadLocal
子类,它的initialValue方法使用提供的Supplier来获取初始值。这提供了一种更简洁的方式来创建带初始值的ThreadLocal
:
java
// 旧方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// 新方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
这种函数式风格更加简洁,也更符合Java 8的编程习惯。
5. 内存泄漏问题深度剖析
5.1 为什么会发生内存泄漏
ThreadLocal
的内存泄漏问题是众所周知的,让我们深入分析这个问题:
java
Thread实例 -> ThreadLocalMap -> Entry -> value
-> Entry -> value
-> Entry -> value (key为null)
当ThreadLocal
对象不再被引用时,Entry中的key变成null(因为是弱引用),但value仍然被强引用。如果线程长时间存活(如线程池中的线程),这些无法访问的value就会一直占用内存,形成泄漏。
具体来说,泄漏发生的条件是:
-
ThreadLocal对象不再被外部引用
-
线程一直存活
-
没有手动调用remove方法清理
在这种情况下,虽然ThreadLocal
对象会被回收,但其关联的value不会被回收,而且无法再通过正常手段访问到这些value。
5.2 弱引用与内存泄漏的关系
ThreadLocalMap
中的Entry继承自WeakReference,key是ThreadLocal
的弱引用:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特性是:当对象只被弱引用引用时,垃圾回收器会在下一次回收中回收该对象。这意味着当ThreadLocal
对象不再被外部强引用时,Entry中的key会变成null。
但这里有一个问题:虽然key会被回收,但value是强引用,不会自动回收。如果没有其他机制处理这些key为null的Entry,就会导致value无法被回收,形成内存泄漏。
ThreadLocalMap
的设计者意识到了这个问题,所以在get、set、remove等操作中添加了清理机制,但这些清理是"顺手"进行的,不保证及时清理所有过期Entry。
5.3 源码中的防泄漏设计
ThreadLocalMap
中包含多种防止内存泄漏的设计,我们前面已经分析过:
-
在set过程中清理:发现key为null的Entry时,会调用replaceStaleEntry或expungeStaleEntry进行清理
-
在get过程中清理:getEntryAfterMiss方法中,发现key为null时会调用expungeStaleEntry清理
-
在remove时清理:remove方法直接调用expungeStaleEntry清理当前和后续的过期Entry
-
在扩容前全量清理:rehash方法首先调用expungeStaleEntries清理所有过期Entry
这些清理机制都是为了减少内存泄漏的可能性,但它们都有一个共同的问题:必须有人调用ThreadLocalMap
的方法才会触发清理。如果一个线程不再使用任何ThreadLocal
,但线程本身还在运行,那么这些过期的Entry就不会被自动清理。
5.4 清理机制的触发时机
让我们总结一下ThreadLocalMap
中清理机制的触发时机:
-
set时触发 :向
ThreadLocalMap
中设置值时,可能触发清理 -
get时触发:获取值时,如果发生哈希冲突,可能触发清理
-
remove时触发:移除值时,必定触发清理
-
resize时触发:扩容前,会进行全量清理
这些清理机制虽然可以减少内存泄漏的可能性,但都不能完全避免泄漏。最安全的做法仍然是在不再需要ThreadLocal
变量时,显式调用remove方法进行清理。
特别是在线程池环境中,线程结束任务后会被重用,如果不清理ThreadLocal
,可能导致数据污染和内存泄漏。因此,ThreadLocal
变量的生命周期应该与任务的生命周期一致,而不是与线程的生命周期一致。