ThreadLocal笔记
ThreadLocal 笔记
- 上图中的threadLocal只有一个对象实例,应该是设置了static
QA
ThreadLocal解决了什么问题,如何解决的
- 多线程并发场景下数据安全问题,一般说来,并发情况下,需要用锁保证数据修改的准确性,效率低,难度大
- ThreadLocal将变量副本封闭到自己的内存空间,对其他线程不可见
ThreadLocal是每个线程都会保留一个变量副本,为什么还会造成冲突呢
- threadLocal的hashCode在threadLocal对象创建时就已经计算好了,hashCode & (len - 1) 本身就会造成冲突
- 也可能是用法不规范,线程内部对同一个threadLocal疯狂set,也不调用remove
ThreadLocalMap是什么
- 是存放ThreadLocal对象的Map容器,实际存放单元为Entry[]
- 类关系为:ThreadLocalEntry,Thread#ThreadLocal.ThreadLocalMap
ThreadLocalMap冲突时采用的寻址方式
- 开放寻址:同一个下标位置发生冲突时,则+1向后寻址,直到找到空位置或过期位置进行存储
ThreadLocalMap使用的hash算法
- Fibonacci散列:
nextHashCode.getAndAdd(HASH_INCREMENT)
HASH_INCREMENT = 0x61c88647
,0x61c88647 = 2 ^ 32 * 0.6180339887- Fibonacci散列公式:(TODO:待补充)
InheritableThreadLocal是什么
- InheritableThreadLocal扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。
- 当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量。
- InheritableThreadLocal 重写了 ThreadLocal 中的 childValue, createMap, getMap 三个方法
为何会内存泄漏
- threadLocal变量使用结束后,
threadLocalRef -> threadLocalObj
强引用消失 - 没有调用remove(),
threadLocalMapObj -> entryObj -> valueObj
强引用仍然存在 - Thread仍然存活,
threadRef -> threadObj -> threadLocalMapObj
强引用仍然存在 - GC发生后,threadLocalMap中的对应entry会变为[null, value],此时value无法被访问到,也没有被回收
为什么要使用WeakReference,强引用不行吗
使用强引用
- threadLocal变量使用结束后,
threadLocalRef -> threadLocalObj
强引用消失 threadRef -> threadObj -> threadLocalMapObj -> entryObj -> threadLocalObj
仍然存在,且都为强引用- 直接造成内存泄漏(threadLocalObj + valueObj),比弱引用更糟(只有valueObj)
- 后续使用中无法判断该entry是否过期,也就无法回收了
使用弱引用
- 内存泄漏(valueObj)
- 后续使用中可以回收泄漏的资源
- get(): getEntry未命中,向后查找,遇到过期entry触发删除-expungeStaleEntry
- set():cleanSomeSlots必定触发,rehash可能触发
- remove():将当前entry标记为过期,使用expungeStaleEntry处理
已泄漏的内存还有机会被清理吗
有机会被清理,分别是
- 探测式清理-expungeStaleEntry(int staleSlot)
- get()触发,如果对应下标位不是目标key,则向后查找,遇到过期的entry调用expungeStaleEntry进行删除
- remove()触发,将当前entry标记为删除,用expungeStaleEntry处理(代码复用,很巧妙)
- 从staleSlot位置开始向后查找,如果遇到过期entry则删除,未过期entry进行搬移,遇到entry==null停止处理,null说明后续位置顺序没问题,不用搬移
- ThreadLocalMap的开放链式寻址决定了如果删除元素必须将错位entry复位,因此不得不向后处理
- 启发式清理-cleanSomeSlots(int i, int n)
- set()触发
- setValue结束后主动调用
- set到一个过期entry时调用replaceStaleEntry(key, value, i),内部会主动调用
- 试探的扫描一些slots,寻找过期元素,扫描次数为 ,找到则调用expungeStaleEntry
- set()触发
为什么建议将ThreadLocal变量设为static
- 确保
threadLocalRef -> threadLocalObj
始终为强引用,且threadLocalRef作为GCRoot不会被回收,始终可以获取到value并调用remove() - 确保 threadLocalObj 只创建一个实例,所有的entry共享该对象,减少内存占用
扩容何时触发
- set()时,如果
cleanSomeSlots
清理之后的size >= threshold,会触发rehash,threshold = len * 2 / 3 - rehash会清理所有过期entry,如果清理后size >= threadhold - threadhold / 4,会触发resize,扩容为2倍大小
扩容是如何操作的
- 首先把数组长度扩容到原来的2倍,oldLen * 2,实例化新数组。
- 遍历for,所有的旧数组中的元素,重新放到新数组中。
- 在放置数组的过程中,如果发生哈希碰撞,则链式法顺延。
- 如果entry已过期,释放entryObj->valueObj强引用(e.value = null)
Netty中的FastThreadLocal是什么
- Netty为了提高ThreadLocal的速度,进行了优化,主要思想是空间换时间
- JDK的ThreadLocal可能会冲突导致线性向后查找,FastThreadLocal直接使用下标位置,避免了冲突
- 代价是需要预先分配一个很大的数组
TerminatingThreadLocal是什么(答案来自GPT)
- TerminatingThreadLocal 是一个 Java 类,用于处理线程本地变量(ThreadLocal)的清理工作。它的作用是在线程结束时自动清理 ThreadLocal 变量,防止内存泄漏。这对于长时间运行的应用程序中使用 ThreadLocal 变量的情况尤为重要。
- 例如,有一个使用 ThreadLocal 变量来存储数据库连接的类。如果不及时清理,这些线程本地的数据库连接引用可能会导致内存泄漏。通过使用 TerminatingThreadLocal,可以确保在线程结束时清理这些数据库连接,从而避免潜在的内存泄漏问题。
- 这个类位于 jdk.internal.misc 包下
源码注释中常提到一个词run,它是什么含义
- run是Entry[]中被两个null夹在中间的子序列
- 探测式清理expungeStaleEntry的作用范围是run的子序列:[run的某个过期entry,run的结束]
- 因为开放链式寻址,expungeStaleEntry删除过期entry后,必须要后续错位的entry进行搬移,遇到null结束
源码解析
初始化
传统的new ThreadLocal<>();
// 构造方法什么都没有做
// 但有部分属性直接被赋值
public ThreadLocal() {
}
// ThreadLocal的hashCode在创建对象时设置,用于计算Entry[]下标
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 固定值2 ^ 32 * 0.6180339887(黄金分割点)
private static final int HASH_INCREMENT = 0x61c88647;
// Fibonacci散列函数
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
1.8新增的withInitial
使用示例
private static final ThreadLocal<SimpleDateFormat> simpledateformatPool = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
// SuppliedThreadLocal定义
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// ThreadLocal#get()时,如果getMap(t) == null,会调用setInitialValue(),其中会使用initialValue()的结果
@Override
protected T initialValue() {
return supplier.get();
}
}
set()
set可能遇到以下几种情况
- 对应slot为空,直接设值即可
- 对应slot不为空,但key相同,说明是更新操作,直接更新
- 对应slot不为空,key不相同(不为空),说明遇到了hash冲突,继续向后查找
- 对应slot不为空,且key为空,说明该slot处的key已经被GC清理,对应entry已经过期,此时进行替换操作
- 将valueObj设置到slot位置
- 如果slot所在的“run”中有其他过期entry,则清理整个“run”
- 启发式清理
- 启发式清理()
- 清理之后如果
size >= threshold
,触发rehash- rehash会清理所有过期的entry,如果清理后
size >= threshold - threshold / 4
则触发扩容
- rehash会清理所有过期的entry,如果清理后
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map如果设置过,直接设置值
map.set(this, value);
} else {
// 第一次set时创建threadLocalMap对象
createMap(t, value);
}
}
// 获取线程的threadLocalMap
// inheritableThreadLocal返回的是t.inheritableThreadLocals;
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private void set(ThreadLocal<?> key, Object value) {
// set() 不像 get() 那样使用快速路径,因为使用 set() 创建新entry至少与替换现有entry一样常见,发生替换时,快速路径通常会失败 。
Entry[] tab = table;
int len = tab.length;
// 直接取模,区别于HashMap的扰动
int i = key.threadLocalHashCode & (len-1);
// 如果该位置e==null,直接设置值(不走循环)
for (Entry e = tab[i];
e != null;
// case3 key不同且未过期,继续向后寻找(拉链法寻址)
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// case2 key相同,覆盖原来的值
e.value = value;
return;
}
if (k == null) {
// case4 key已过期,替换过期的entry,涉及到清理,放到清理那块吧
replaceStaleEntry(key, value, i);
return;
}
}
// case1 空位置直接插入
tab[i] = new Entry(key, value);
int sz = ++size;
// 清理
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 找下个索引,类似循环数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 找上个索引,类似循环数组
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
get()
- 如果slot处的key==目标值,说明直接命中,返回对应的valueObj即可
- 如果未命中则继续向后找,顺便清理过期的entry
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;
}
}
// 使用ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));会触发
return setInitialValue();
}
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)
// 找到对应entry
return e;
if (k == null)
// entry已过期(key被gc回收),删除过期entry
expungeStaleEntry(i);
else
// 向后寻找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove()
remove 并不是简单将位置 i 释放,而是先释放软引用(将当前entry标记为过期),然后调用expungeStaleEntry处理
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
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) {
// 释放threadLocal对象,标记为已过期
e.clear();
// 处理当前entry
expungeStaleEntry(i);
return;
}
}
}
扩容
private void rehash() {
// 删除所有过期entry
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
// 清理之后的 size >= threshold * 0.75,执行扩容
resize();
}
// 对每个过期的entry位置都调用expungeStaleEntry
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);
}
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 新的下标位置
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
// 开放链式找到插入可用位置
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
// 设置阈值为长度的 2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
清理
- set操作时,如果遇到已过期的entry,会调用
replaceStaleEntry
,该方法会清理部分过期的entry
/**
* set()时如果遇到过期entry,将value设置到该位置(即使有其他entry也是这个key)
* 方法副作用:删除staleSlot所在“run”的所有过期entry
* “run” 是两个空slot(entry==null)之间的子序列
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 往前找,找到"run"中第一个过期的slot
// 本方法会一次性检查并清理整个"run",避免GC清理大量过期slot导致的频繁rehash
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到key所在的entry,或者"run"中下一个null位置(不管是不是第一个)
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到key,需要将key所在entry和过期entry互换位置,以保证hash table的顺序
// 对于新检测到的过期entry,可以随后使用expungeStaleEntry删除
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果slotToExpunge("run"的第一个过期slot,清理起始位置)==staleSlot,因为已经将 i 和 staleSlot 位置互换,所以从 i 清理就可以了
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理"run",顺便再随机清理下,然后返回
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// staleSlot之前没有其他的过期slot(仍从staleSlot开始清理),当前i位置的slot已经过期,改为从i位置开始清理
// 因为staleSlot位置要设置valueObj,所以就不需要被清理了
// 如果staleSlot之前仍有过期的slot,就无需操作,因为清理会将本"run"的过期slot都处理掉(包括当前的过期位置i)
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 找不到key,将过期entry的value置为null,并在该slot放一个新entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果"run"中存在其他过期entry,直接干掉,顺便启发式清理一波
// !=说明只有staleSlot过期了,但它已经设置了新entry,所以就不用再清理了
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 删除过期entry
* 参数 staleSlot 为过期entry的下标
* 返回 下一个可用位置i(i>staleSlot)
* 简单删除是不够的,还需要冲突的链式entry前移,因此需要继续向后查找
* 直到遇到tab[i]==null停止(也就是一个完整的"run",staleSlot会被标记为过期,也就是作为"run"的开始),null说明后续位置正常
* [staleSlot,i]之间的数据都需要修复:删除过期entry,搬移正常entry
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 删除staleSlot位的valueObj强引用
tab[staleSlot].value = null;
// 删除staleSlot位
tab[staleSlot] = null;
size--;
// 继续向后处理entry,直到遇到null,null说明后续数据正常,不需要继续处理
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 删除过期entry
e.value = null;
tab[i] = null;
size--;
} else {
// 尚未过期,向前搬移
// 原始下标位h
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// e应该在h处,现在位于i,需要搬移,将i位置空出来
tab[i] = null;
// 从原始位置开始,向后找到第一个空位,放置e
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 参数 i:尚未过期的entrySlot,清理将从i+1处开始
* 参数 n: 限制执行次数为 log(n)
* 返回: 是否有元素删除
* 抽取部分元素检查是否过期,如果已过期则删除
* 添加新元素,或者删除过期元素时调用
* 不限制 log(n) 的话只能逐元素检查,复杂度为 O(n),无法接受
* 完全不检查的话,垃圾就太多了
* 因此采用折中方式 log(n)
*/
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) {
// e已过期
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
典型场景
- SimpleDateFormat
- 链路追踪
- Http请求
- 连接池信息
使用示例
@Slf4j
public class DateFormatter {
private static final ThreadLocal<SimpleDateFormat> simpledateformatPool = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
public static String formatSimple(Date date) {
if (date == null) {
return "";
}
String result = "";
try {
result = simpledateformatPool.get().format(date);
} catch (Exception e) {
logger.error("happen error when format date : {}", date);
}
return result;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署