恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战
首页
资讯中心
/
源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战
源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战
发布时间:2026/9/12 14:35:00
源码级剖析 Java ThreadLocal线程局部变量的存储结构、哈希探测与内存回收实战【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunterThreadLocal 是 Java 并发编程中最常用也最容易被误用的工具之一。本文以 JDK 源码为线索从 Thread 类 中的threadLocals字段出发完整剖析ThreadLocal的set/get/remove核心实现、ThreadLocalMap的哈希存储与扩容机制以及弱引用Entry与内存泄漏之间的关系。读完你不仅能看懂ThreadLocal的每一行关键代码还能掌握线程池与 Web 容器场景下线程局部变量的正确清理方式并顺带理解 Netty 的FastThreadLocal为什么更快。ThreadLocal 是什么一句话理解线程局部变量ThreadLocal类提供了线程局部变量thread-local variables的get/set实现。它与普通成员变量最大的不同在于每个线程都可以通过同一个ThreadLocal对象 get/set 出属于自己的专属值线程之间互不可见、互不干扰。ThreadLocal实例通常是类中的私有静态变量常用于将状态与线程关联典型场景包括用户 ID、事务 ID 等请求维度的上下文传递数据库连接、Session 等一线程一实例的资源持有框架内部如 Spring 的RequestContextHolder、MyBatis 的 SqlSession 管理的隐式传参。tips在类中定义ThreadLocal变量时一般在定义时就进行实例化避免多次创建或空指针问题。如上图所示Thread1与Thread2各自持有一个独立的ThreadLocalMap即使它们操作的是同一个threadLocalA对象存储的值也完全独立valueA1与valueA2。这正是同一个 ThreadLocal 对象能为每个线程绑定专属值的奥秘——set的值实际上被存到了各个调用线程自己的ThreadLocalMap中。溯源Thread 类中的 threadLocals 字段在阅读ThreadLocal核心 API 之前必须先看Thread类本身。因为ThreadLocal的get/set方法操作的其实都是Thread类中的成员变量。仓库中的 Thread 类源码分析 对这一点做了详细铺垫public class Thread implements Runnable { /** 线程名 */ private volatile char name[]; /** 优先级 */ private int priority; /** 是否为守护线程 */ private boolean daemon; /** 线程要执行的目标任务 */ private Runnable target; /** 所属线程组 */ private ThreadGroup group; /** 类加载器 */ private ClassLoader contextClassLoader; /** * ThreadLocal 能为线程设置线程私有变量 就是通过下面这个threadLocals变量完成的 * ThreadLocal的get/set方法就是通过操作 各个线程的 threadLocals 变量实现的。 * 1、线程A持有一个 ThreadLocalMap 变量 * 2、线程A调用一个类的 ThreadLocal变量 tlA 的 get/set方法 * 3、tlAThreadLocal的 get/set方法 获取当前线程A调用 线程A 的 ThreadLocalMap变量 的get/put方法 * 4、其它线程 调用 tlAThreadLocal的 get/set方法 同理。 */ ThreadLocal.ThreadLocalMap threadLocals; ThreadLocal.ThreadLocalMap inheritableThreadLocals; /** 线程栈的大小 */ private long stackSize; ... }可以看到Thread类持有两个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals每个线程自己的线程局部变量表是ThreadLocal存取的主战场inheritableThreadLocals可继承的线程局部变量表用于子线程继承父线程的变量后文详述。线程对象在初始化时这两个字段均为null直到第一次调用某个ThreadLocal的set或get时才被懒加载创建。ThreadLocal本身不存储任何数据它只是钥匙——真正的锁柜是每个线程各自的ThreadLocalMap。ThreadLocal 核心 API 源码解析有了上面的铺垫ThreadLocal的源码就非常直观了。以下代码与注释来自仓库文档 ThreadLocal.md 的完整继承与解读。set(T value)把值写入当前线程public class ThreadLocalT { /** * ThreadLocal能为每个 Thread线程 绑定一个专属值的奥秘就是 * 每个Thread对象都持有一个 ThreadLocalMap类型的成员变量其key为ThreadLocal对象 * value为绑定的值所以每个线程调用 ThreadLocal对象 的set(T value)方法时都会将 * 该ThreadLocal对象和绑定的值 以键值对的形式存入当前线程这样同一个ThreadLocal对象 * 就可以为每个线程绑定一个专属值咯。 * 每个线程调用 ThreadLocal对象的get()方法时就可以根据 当前ThreadLocal对象 get到 绑定的值。 */ public void set(T value) { // 获取当前线程 Thread t Thread.currentThread(); // 获取当前线程对象中持有的 ThreadLocalMap类型的成员变量 // ThreadLocalMap看名字也知道它是一个 Map类型的 类 ThreadLocalMap map getMap(t); if (map ! null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { // 经过前面对 Thread类 源码的分析可以知道Thread类中有一个 ThreadLocalMap 类型的 // threadLocals变量 return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals new ThreadLocalMap(this, firstValue); } ... }set的完整调用链为ThreadLocal.set(value)→Thread.currentThread()拿到当前线程 →getMap(t)取出该线程的threadLocals→ 若map已存在则map.set(this, value)否则先通过createMap创建ThreadLocalMap并放入首个键值对。get()从当前线程取出专属值public T get() { Thread t Thread.currentThread(); ThreadLocalMap map getMap(t); if (map ! null) { // 通过当前 ThreadLocal对象获取绑定的值 ThreadLocalMap.Entry e map.getEntry(this); if (e ! null) { SuppressWarnings(unchecked) T result (T)e.value; return result; } } return setInitialValue(); }get的逻辑与set对称先取当前线程的ThreadLocalMap若 map 存在且能通过当前ThreadLocal对象命中对应的Entry直接返回其value若 map 不存在或没有命中则调用setInitialValue()。setInitialValue()在 JDK 源码中的实现如下它调用可被子类重写的initialValue()方法获取初始值默认返回null然后走与set相同的创建/写入路径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()未显式set时会返回null或你重写initialValue()提供的默认值同时它也会触发ThreadLocalMap的懒创建。remove()显式解除绑定public void remove() { // 获取当前线程的ThreadLocalMap成员变量不为空就将当前 ThreadLocal对象 // 对应的 键值对 remove掉 ThreadLocalMap m getMap(Thread.currentThread()); if (m ! null) m.remove(this); }remove直接删除当前线程ThreadLocalMap中与该ThreadLocal对象对应的键值对。它是防止内存泄漏的关键手段后文会重点强调其使用时机。ThreadLocalMap线程局部变量的存储仓ThreadLocalMap是ThreadLocal的静态内部类也是整个机制的核心。与大部分Map实现相同它底层使用动态数组保存键值对Entry同样具备rehash、resize等操作但它与HashMap有两个显著差异key 被固定为ThreadLocal类型且采用开放寻址法线性探测解决哈希冲突。Entry弱引用 Key 的设计static class ThreadLocalMap { /** * 存储键值对key 为 ThreadLocal对象value 为 与该ThreadLocal对象绑定的值 * Entry的key是对ThreadLocal的弱引用当抛弃掉ThreadLocal对象时垃圾收集器会 * 忽略这个key的引用而清理掉ThreadLocal对象防止了内存泄漏 */ static class Entry extends WeakReferenceThreadLocal? { Object value; Entry(ThreadLocal? k, Object v) { super(k); value v; } } ... }Entry继承自WeakReferenceThreadLocal?即keyThreadLocal 对象是弱引用value 是强引用。这样设计的好处是当外部不再强引用某个ThreadLocal对象时GC 可以回收该 key避免ThreadLocal对象本身长期驻留内存。但代价也随之而来——value 仍然是强引用只要线程线程池中的线程存活即使 key 已被回收e.get() nullvalue 也不会被自动回收从而形成key 为 null 的陈旧条目这就是 ThreadLocal 内存泄漏的根源。哈希定位与线性探测// 看过 HashMap 或 ConcurrentHashMap 源码的同学 一定下面对这些代码很眼熟 /** * 数组初始容量 */ private static final int INITIAL_CAPACITY 16; /** * Entry数组用于存储 ThreadLocal? k, Object v键值对 */ private Entry[] table; /** * Entry元素数量 */ private int size 0; /** * 类似于 HashMap 扩容因子机制 */ private int threshold; // Default to 0 private void setThreshold(int len) { threshold len * 2 / 3; } 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); }初始容量INITIAL_CAPACITY 16负载因子阈值不是 0.75 而是2/3threshold len * 2 / 3比 HashMap 更保守这是为了给线性探测留出更多的空槽位、降低探测链长度nextIndex/prevIndex实现了环形数组遍历到数组末尾时回绕到下标 0这就是开放寻址法线性探测的继续往后找逻辑。每个ThreadLocal对象都持有一个threadLocalHashCode在 JDK 源码中它由静态AtomicInteger按固定增量0x61c88647黄金分割数相关的斐波那契散列增量递增生成能保证哈希值在数组长度取模后分布足够均匀。定位公式为int i key.threadLocalHashCode (len - 1);构造方法首次创建的两种入口/** * 系列构造方法 */ 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); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable parentMap.table; int len parentTable.length; setThreshold(len); table new Entry[len]; for (int j 0; j len; j) { Entry e parentTable[j]; if (e ! null) { SuppressWarnings(unchecked) ThreadLocalObject key (ThreadLocalObject) e.get(); if (key ! null) { Object value key.childValue(e.value); Entry c new Entry(key, value); int h key.threadLocalHashCode (len - 1); while (table[h] ! null) h nextIndex(h, len); table[h] c; size; } } } }第一个构造方法用于线程首次set/get时创建直接把首个键值对放入哈希位置。第二个私有构造方法接收一个parentMap父线程的inheritableThreadLocals逐条复制父线程的键值对并对每个值调用key.childValue(e.value)。childValue的默认实现是原样返回而InheritableThreadLocal重写了该方法从而实现了子线程继承父线程线程局部变量的能力——这正是inheritableThreadLocals发挥作用的地方详见下文。set()覆盖、替换陈旧条目与触发 rehash/** * 常规Map实现类 的set()方法只不过这里的 key被规定为 ThreadLocal类型 */ 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(); // 如果key相等覆盖value if (k key) { e.value value; return; } // 如果key为null用新key、value覆盖同时清理历史keynull的陈旧数据 if (k null) { replaceStaleEntry(key, value, i); return; } } tab[i] new Entry(key, value); int sz size; // 若超过阀值则rehash if (!cleanSomeSlots(i, sz) sz threshold) rehash(); }set的核心流程从哈希位置开始线性探测若遇到 key 相同的Entry直接覆盖 value 并返回若遇到k null的陈旧条目key 已被 GC 回收调用replaceStaleEntry用新键值对替换同时清理探测链上的其他陈旧数据若探测一圈后找到空位则放入新Entrysize自增若本次插入未能通过cleanSomeSlots启发式清理消除足够的陈旧条目且size threshold则触发rehash()。可见 ThreadLocal 在每次set时都会顺手做内存清理这也是key 弱引用 操作时清理这套防泄漏机制的完整闭环。getEntry() 与探测失败后的补救/** * 根据 ThreadLocal对象 获取其对应的 Entry实例 */ 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); }哈希位置直接命中e.get() key时 O(1) 返回否则调用getEntryAfterMiss沿着探测链继续查找过程中若遇到e.get() null的陈旧条目同样会调用expungeStaleEntry(i)顺手清理——每次get也在做内存回收。remove()清除并整理/** * Remove the entry for key. */ 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); return; } } }remove找到目标Entry后执行e.clear()清掉弱引用 key再调用expungeStaleEntry(i)清理该位置的陈旧条目并重整探测链保证后续探测不被空洞打断。rehash 与 resize清理 扩容/** * 调整当前table的容量。首先扫描整个容器以删除过时的条目如果这不能充分缩小表的大小 * 将进行扩容操作 */ private void rehash() { // 扫描整个容器删除过时的条目 expungeStaleEntries(); // 若未能充分缩小表的大小则进行扩容操作 if (size threshold - threshold / 4) resize(); } /** * 扩容为原容量的两倍 */ private void resize() { Entry[] oldTab table; int oldLen oldTab.length; int newLen oldLen * 2; Entry[] newTab new Entry[newLen]; int count 0; // 遍历Entry[]数组 for (int j 0; j oldLen; j) { Entry e oldTab[j]; if (e ! null) { ThreadLocal? k e.get(); // 如果keynull把value也置null,有助于GC回收对象 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; }rehash先做全表expungeStaleEntries()清理陈旧条目若清理后size仍达到threshold - threshold / 4即阈值的 3/4才执行resizeresize将容量翻倍newLen oldLen * 2遍历旧表重新哈希陈旧条目直接把value置null帮助 GC有效条目按新长度重新线性探测入位与HashMap的链表重排不同ThreadLocalMap 扩容后所有有效条目都要按新容量重新探测放置这也是后续 NettyFastThreadLocal想要规避的开销之一。子线程传值InheritableThreadLocal 与 Thread.init前面提到Thread类还有一个inheritableThreadLocals字段。在 Thread 类源码分析 的init()方法中可以看到子线程是如何继承父线程变量的private void init(ThreadGroup threadgroup, Runnable runnable, String name, long l, AccessControlContext accesscontrolcontext) { ... // 当前线程就是该线程的父线程 Thread parent currentThread(); ... target runnable; setPriority(priority); if (parent.inheritableThreadLocals ! null) // 创建线程共享变量副本 inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); stackSize l; // 分配线程id tid nextThreadID(); }关键逻辑创建新线程时currentThread()即为父线程若父线程的inheritableThreadLocals不为 null则调用ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)内部就是走前文分析的ThreadLocalMap(ThreadLocalMap parentMap)私有构造方法配合InheritableThreadLocal.childValue()完成值复制因此只有继承自InheritableThreadLocal的变量才能被子线程感知普通ThreadLocal的数据放在threadLocals中子线程无法访问。这一点在框架中非常实用例如需要在新建线程时自动携带主线程的 traceId、用户上下文等场景。但要注意继承发生在线程创建的那一刻之后父线程对InheritableThreadLocal的修改不会同步给已创建的子线程。使用注意事项与实战规范原文档在结尾给出了两条非常重要的使用铁律这里结合源码展开说明。1. ThreadLocal 不是用来解决线程安全问题的ThreadLocal 不是用来解决线程安全问题的多线程不共享不存在竞争其目的是使线程能够使用本地变量。从源码可以清晰看到set/get操作的数据都落在当前线程自己的ThreadLocalMap里线程之间物理隔离自然不存在共享与竞争也就谈不上解决线程安全问题。如果你的业务对象本身需要被多个线程共享并修改ThreadLocal 并不能替你保证线程安全——它只负责把数据藏到各线程自己的储物柜里。另外ThreadLocal 变量本身ThreadLocal 对象是共享的线程之间共享的是这把钥匙而不是柜子里的值。2. 线程池场景必须 remove否则可能内存泄漏或数据串扰项目如果使用了线程池那么线程回收后 ThreadLocal 变量要 remove 掉否则线程池回收线程后变量还在内存中可能会带来意想不到的后果结合源码理解这背后的机制线程池中的线程执行完任务后并不会销毁而是回到池中等待复用只要线程存活它持有的ThreadLocalMap就存活即使ThreadLocal的 key 已被弱引用回收value 仍是强引用无法被 GC 回收——这是内存泄漏的根源需要线程存活 key 被回收两个条件同时满足更隐蔽的风险是数据串扰同一个复用线程执行的下一个任务可能继承上一个任务遗留的 ThreadLocal 值读到过期甚至错误的数据。正确的姿势是在每个业务处理结束点finally块或容器拦截器显式调用remove()try { UserContextHolder.set(currentUser); doBusiness(); } finally { UserContextHolder.remove(); // 无论业务成功失败都要清理 }对于 Tomcat 容器的线程池场景原文档给出了经典方案继承HandlerInterceptorAdapter复写afterCompletion()方法完成清理public class ThreadLocalCleanInterceptor extends HandlerInterceptorAdapter { Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求处理完成后清理当前线程Tomcat 工作线程上绑定的 ThreadLocal 变量 // 防止变量残留在线程池复用线程中避免内存泄漏与后续请求的数据串扰 UserContextHolder.remove(); super.afterCompletion(request, response, handler, ex); } }随后在 Spring MVC 配置中注册该拦截器即可。afterCompletion在整个请求处理链路含异常结束后被回调正好对应 Tomcat 工作线程归还线程池的时机。注HandlerInterceptorAdapter在 Spring 5.3 起已标记为废弃新项目可直接实现HandlerInterceptor接口并用其default方法思路完全一致。延伸阅读Netty FastThreadLocal 的优化思路理解了原生 ThreadLocal 的哈希 线性探测 扩容重哈希实现后就能更好地理解为什么 Netty 要自研 FastThreadLocal。仓库中的FastThreadLocal源码分析.md指出原生 ThreadLocal 的每次存取都要经历计算threadLocalHashCode→ 与容量取模定位 → 若冲突则线性探测。而FastThreadLocal换了一条路每个FastThreadLocal在构造时通过InternalThreadLocalMap.nextVariableIndex()从全局自增计数器拿到一个固定下标存取时直接用该下标访问Object[] indexedVariables数组的对应位置public FastThreadLocal() { index InternalThreadLocalMap.nextVariableIndex(); }其性能优势正是针对原生 ThreadLocal 的三个开销点定位更快数组下标直接访问免去了哈希计算与冲突探测扩容更简单数组扩容只需复制原内容并用占位对象填充无需像 ThreadLocalMap 那样对全部有效条目重新哈希仍可能二次冲突遍历与回收更可控所有FastThreadLocal的引用被统一保存在数组首位集合中通过FastThreadLocal.removeAll()一次性清理全部变量配合 NettyDefaultThreadFactory对任务执行完的自动清理finally { FastThreadLocal.removeAll(); }在避免内存泄漏的同时省去了原生 ThreadLocal 每次操作后的启发式清理开销。这恰好从反面印证了原生 ThreadLocal 设计中弱引用 操作时启发式清理这套机制的成本与必要性。总结回到核心结论ThreadLocal 本身不存数据它只是 key真正存储数据的ThreadLocalMap挂在每个Thread的threadLocals字段上天然实现线程隔离set线性探测写入、get哈希定位读取、remove显式清除底层是容量 16、阈值 2/3、冲突线性探测的动态Entry[]数组Entry的 key 为弱引用value 为强引用——这既是防泄漏设计也是泄漏隐患的来源因此每次 set/get/remove 都会伴随陈旧条目清理InheritableThreadLocal通过Thread.init()中的createInheritedMap实现子线程继承线程池 / Web 容器复用线程的场景下务必在业务结束点remove()这是源码层面可以明确推导出的硬性规范。如果想继续深入仓库还提供了关联的 Thread 类源码分析理解threadLocals的完整上下文与 Netty FastThreadLocal 源码分析高性能线程局部变量的另类实现两者对照阅读对线程局部变量机制的理解会更立体。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考