ThreadLocal源码解析

发布时间:2026/7/18 20:34:22

ThreadLocal源码解析 一. ThreadLocal简介在日常的 Java 开发中我们经常会遇到这样一类问题如何在不增加方法参数传递成本的情况下在同一个线程的多个方法调用之间共享数据例如请求链路中的用户信息、数据库连接、事务上下文等这些数据本质上是“与线程绑定”的但又不适合通过全局共享变量来管理因为全局变量在多线程环境下会引发并发安全问题。这个时候ThreadLocal就成为了一种非常优雅且高效的解决方案。ThreadLocal的核心作用可以用一句话概括为每个线程提供一份独立的变量副本实现线程之间的数据隔离。与我们平时使用的共享变量不同ThreadLocal并不是让多个线程去竞争同一份数据而是让每个线程都拥有属于自己的“私有变量”从而避免了加锁、竞争等并发问题。这种设计本质上是一种“用空间换时间”的思想通过牺牲一定的内存来换取更高的执行效率和更简单的编程模型。在实际业务开发中ThreadLocal的使用场景非常广泛。例如在 Web 请求处理中我们可以通过ThreadLocal保存当前登录用户的信息这样在整个请求处理链路中无需层层传递参数就可以随时获取用户上下文在数据库访问中可以将 Connection 绑定到当前线程实现同一线程内的事务一致性在一些框架如 Spring、MyBatis中ThreadLocal 更是被大量用于管理线程级别的资源和上下文信息。可以说ThreadLocal是构建“线程上下文Thread Context”的核心工具之一。二. ThreadLocal使用示例ThreadLocal的使用简单示例java/** * 线程上下文ThreadLocal 实现 * * 用于在同一个线程内共享请求级数据例如用户信息、请求ID等 */publicfinalclassThreadContext{/** * 每个线程独享一份 RequestContext */privatestaticfinalThreadLocalRequestContextCONTEXTThreadLocal.withInitial(RequestContext::new);privateThreadContext(){}/** * 获取当前线程的上下文对象 */publicstaticRequestContextgetContext(){returnCONTEXT.get();}/** * 设置用户信息 */publicstaticvoidsetUserInfo(UserInfouserInfo){CONTEXT.get().setUserInfo(userInfo);}/** * 获取用户信息 */publicstaticUserInfogetUserInfo(){returnCONTEXT.get().getUserInfo();}/** * 设置请求ID */publicstaticvoidsetRequestId(StringrequestId){CONTEXT.get().setRequestId(requestId);}/** * 获取请求ID */publicstaticStringgetRequestId(){returnCONTEXT.get().getRequestId();}/** * 清理 ThreadLocal必须在请求结束时调用 */publicstaticvoidremove(){CONTEXT.remove();}}/** * 请求上下文对象 * * 存储与当前请求相关的数据线程隔离 */publicclassRequestContext{/** * 用户信息 */privateUserInfouserInfo;/** * 请求ID */privateStringrequestId;// getter / setter publicUserInfogetUserInfo(){returnuserInfo;}publicvoidsetUserInfo(UserInfouserInfo){this.userInfouserInfo;}publicStringgetRequestId(){returnrequestId;}publicvoidsetRequestId(StringrequestId){this.requestIdrequestId;}}三. 源码解析1. get() 的核心源码/** * 获取当前线程中与这个 ThreadLocal 关联的值 */publicTget(){// 1. 先拿到当前线程对象ThreadtThread.currentThread();// 2. 再从当前线程中取出 ThreadLocalMapThreadLocalMapmapgetMap(t);// 3. 如果当前线程已经有 ThreadLocalMap则尝试根据 this 作为 key 查找if(map!null){ThreadLocalMap.Entryemap.getEntry(this);if(e!null){SuppressWarnings(unchecked)Tresult(T)e.value;returnresult;}}// 4. 如果当前线程还没有 map或者 map 中没有当前 key// 就进入初始化流程returnsetInitialValue();}/** * 获取某个线程内部持有的 ThreadLocalMap * * 注意 * ThreadLocalMap 是挂在线程对象 Thread 上的 * 不是挂在 ThreadLocal 自己身上的。 */ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}/** * 当前线程第一次访问该 ThreadLocal 时会走这个逻辑 */privateTsetInitialValue(){TvalueinitialValue();ThreadtThread.currentThread();ThreadLocalMapmapgetMap(t);if(map!null){map.set(this,value);}else{createMap(t,value);}returnvalue;}/** * 当线程第一次使用 ThreadLocal并且 threadLocals 还不存在时 * 创建一个新的 ThreadLocalMap */voidcreateMap(Threadt,TfirstValue){t.threadLocalsnewThreadLocalMap(this,firstValue);}2. set() 的核心源码/** * 给当前线程设置一个与该 ThreadLocal 关联的值 */publicvoidset(Tvalue){// 1. 获取当前线程ThreadtThread.currentThread();// 2. 获取当前线程内部的 ThreadLocalMapThreadLocalMapmapgetMap(t);if(map!null){// 3. 如果 map 已存在则直接放入map.set(this,value);}else{// 4. 如果 map 不存在则为当前线程创建一个新的 mapcreateMap(t,value);}}也就是说ThreadLocal并没有“自己保存一份值”而是把值交给当前线程去保存。因此不同线程调用同一个ThreadLocal.set()最终写入的是各自线程内部的独立数据互不影响。publicclassThreadimplementsRunnable{/** * 当前线程持有的 ThreadLocalMap * * 这个字段专门存放普通 ThreadLocal 的数据。 * 每个线程对象都有自己独立的一份。 */ThreadLocal.ThreadLocalMapthreadLocalsnull;/** * 这个字段用于 InheritableThreadLocal * * 父线程创建子线程时可以把值传递给子线程。 */ThreadLocal.ThreadLocalMapinheritableThreadLocalsnull;}很多人刚接触 ThreadLocal 时会误以为ThreadLocal内部维护了一个Mapkey是线程value是数据。其实正好相反。真实结构是每个线程内部维护一个ThreadLocalMapkey是ThreadLocalvalue是线程私有数据。这也是为什么ThreadLocal能做到线程隔离的根本原因。3. ThreadLocalMap 的 Entry 结构staticclassThreadLocalMap{/** * Entry 继承 WeakReference * * 也就是说 * keyThreadLocal是弱引用 * value真正保存的数据是强引用 */staticclassEntryextendsWeakReferenceThreadLocal?{/** * 与 ThreadLocal 关联的实际值 */Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);// 弱引用保存 keyvaluev;// 强引用保存 value}}}ThreadLocalMap.Entry并不是普通的键值对而是做了一个特殊设计keyWeakReferenceThreadLocal?value普通强引用对象也就是说key是弱引用value是强引用。这样设计的目的是为了避免某个ThreadLocal对象已经不用了却因为还被ThreadLocalMap强引用着而无法回收。但这也带来了一个副作用如果ThreadLocal被回收了而线程还活着那么Entry就可能变成keynullvalue业务对象这就是所谓的“脏Entry”如果不及时清理就可能造成内存长期占用。4. ThreadLocalMap 的底层数组结构staticclassThreadLocalMap{/** * 初始容量必须是 2 的幂 */privatestaticfinalintINITIAL_CAPACITY16;/** * 真正存储数据的数组 * * 每个元素都是一个 Entry * key ThreadLocal弱引用 * value 线程私有数据 */privateEntry[]table;/** * 当前数组中已使用的 Entry 数量 */privateintsize0;/** * 扩容阈值 */privateintthreshold;/** * 创建 ThreadLocalMap并把第一条数据直接放进去 */ThreadLocalMap(ThreadLocal?firstKey,ObjectfirstValue){tablenewEntry[INITIAL_CAPACITY];// 计算数组下标intifirstKey.threadLocalHashCode(INITIAL_CAPACITY-1);// 存入第一条 Entrytable[i]newEntry(firstKey,firstValue);size1;// 设置扩容阈值thresholdsetThreshold(INITIAL_CAPACITY);}}ThreadLocalMap的底层不是HashMap而是一个定制化的数组结构。它使用数组来保存Entry并通过threadLocalHashCode (len - 1)的方式定位下标。由于数组长度总是 2 的幂因此这种位运算定位效率很高。同时ThreadLocalMap没有采用链表法解决冲突而是采用了开放地址法也就是冲突后继续往后找空位。这样做的好处是结构更紧凑减少链表节点带来的额外对象和指针跳转更有利于 CPU cache 命中对ThreadLocal这种典型小规模存储场景更加高效5. ThreadLocalMap 的 getEntry()如何查找元素staticclassThreadLocalMap{/** * 根据 ThreadLocal key 获取对应 Entry */privateEntrygetEntry(ThreadLocal?key){// 先根据 hash 直接定位一个下标intikey.threadLocalHashCode(table.length-1);Entryetable[i];// 如果正好命中直接返回if(e!nulle.get()key){returne;}// 如果没命中进入线性探测returngetEntryAfterMiss(key,i,e);}/** * 发生哈希冲突后继续向后查找 */privateEntrygetEntryAfterMiss(ThreadLocal?key,inti,Entrye){Entry[]tabtable;intlentab.length;while(e!null){ThreadLocal?ke.get();// 找到了对应的 keyif(kkey){returne;}// 如果发现 key 已经被 GC 回收了则顺便清理脏数据if(knull){expungeStaleEntry(i);}else{// 继续向后找下标到头后回环到 0inextIndex(i,len);}etab[i];}returnnull;}/** * 线性探测下一个下标 */privatestaticintnextIndex(inti,intlen){return((i1len)?i1:0);}}ThreadLocalMap查找元素时先根据hash值定位数组下标。如果当前位置不是目标key就说明发生了冲突这时不会像HashMap那样走链表而是采用线性探测从当前位置往后逐个查找数组到尾后回到头部继续找直到找到目标 key 或遇到空槽位为止同时如果在线性探测过程中发现某个Entry的key已经变成null说明这个ThreadLocal已被 GC 回收此时还会顺带触发脏数据清理。ThreadLocalMap没有使用HashMap的链表或红黑树结构而是采用了数组加开放地址法线性探测。这种设计不会导致查找错误因为插入和查找都遵循同样的探测路径从hash位置开始逐步向后查找直到找到目标key或空槽位。虽然理论上可能退化为线性扫描但ThreadLocalMap的数据量通常很小加上使用黄金分割hash使分布均匀因此冲突概率很低。相比HashMap这种结构的优势在于使用连续内存CPU cache 友好没有链表指针减少内存访问跳跃对象数量更少降低 GC 压力因此它更适合ThreadLocal这种“小数据、高频访问”的场景而不是通用的大规模数据存储。6. ThreadLocalMap 的 set()如何插入元素staticclassThreadLocalMap{/** * 向当前线程的 ThreadLocalMap 中设置值 */privatevoidset(ThreadLocal?key,Objectvalue){Entry[]tabtable;intlentab.length;// 先根据 key 的 hash 定位下标intikey.threadLocalHashCode(len-1);// 使用开放地址法处理冲突for(Entryetab[i];e!null;etab[inextIndex(i,len)]){ThreadLocal?ke.get();// 如果 key 已存在则直接覆盖 valueif(kkey){e.valuevalue;return;}// 如果发现某个脏 Entrykey 已被 GC 回收// 则进入替换脏槽位的逻辑if(knull){replaceStaleEntry(key,value,i);return;}}// 找到空槽位直接插入tab[i]newEntry(key,value);intszsize;// 插入后尝试清理脏 Entry如果没清理掉太多且容量达到阈值则扩容if(!cleanSomeSlots(i,sz)szthreshold){rehash();}}}插入过程可以概括为三种情况第一种如果当前 key 已经存在那么直接覆盖旧值。第二种如果探测过程中发现某个槽位的 key 已经是 null说明这里存在脏 Entry此时不会简单跳过而是触发专门的替换和清理逻辑。第三种如果一路探测后找到了空槽位就直接插入新的 Entry。这说明ThreadLocalMap在写入时不仅做插入还会顺带做一部分垃圾清理工作。这也是它“懒清理”设计的一部分。四. 为什么ThreadLocalMap的key为什么要使用弱引用ThreadLocalMap的key使用弱引用是为了避免ThreadLocal对象失去外部引用后仍然被Thread持有导致无法被 GC 回收从而产生内存泄漏。ThreadLocal│ │作为 key │ │ │Thread线程 ↓ └──ThreadLocalMap└──Entry[]├── keyThreadLocal弱引用 └── value业务数据强引用上面是ThreadLocal的结构关系图从线程角度看应该是ThreadLocal入口 │ ├───────────────────────────┐ │ │ ↓ ↓ 线程A线程B│ │ ↓ ↓ThreadLocalMapThreadLocalMap│ │ ├──Entry├──Entry│ keyThreadLocal│ keyThreadLocal│ valueA│ valueB从上面的结构中可以看出我们常用的ThreadLocal可以是static final的也可以是我们随意创建的但是Thread强引用ThreadLocalMapThreadLocalMap强引用EntryEntrykey 弱引用ThreadLocalvalue 强引用对象如果ThreadLocal的key是强引用当有如下代码ThreadLocaltlnewThreadLocal();tl.set(newBigObject());// 这里把引用断掉tlnull;这个时候GC回收不了创建的这个ThreadLocal tl原因就是引用关系是Thread → ThreadLocalMap → Entry → key(ThreadLocal)。所以Entry的key被设计成了弱引用当是弱引用时GC回收时ThreadLocal就可以被回收了Entry.key → 变成 null同时结构变成Entry(null, value)所以就会有新的问题value还在。因为value是强引用关系是Thread → ThreadLocalMap → Entry → value。所以我们在使用ThreadLocal的时候一定要注意及时清理如果不调用remove仍然可能出现value无法释放的问题。因此ThreadLocal采用的是“弱引用 懒清理”的策略并要求开发者在使用完后手动调用remove特别是在使用线程池时(线程复用)。那么为啥不把value设置成弱引用呢因为弱引用在一次GC之后会被回收所以value不能是弱引用那么这个时候你会不会想到那为啥key作为弱引用就不GC回收呢原因是我们使用的时候创建的ThreadLocal都是在代码中有引用的或者像文章开头的例子中一样使用static修饰这个时候我们创建的ThreadLocal引用是一直存在的所以ThreadLocal作为弱引用的key不会担心被回收。五. 总结ThreadLocal的设计本质上并不是“一个全局容器按线程存值”而是“每个线程内部维护一个私有Map”。其中ThreadLocal自身只负责提供访问入口和key真正的数据存储在线程对象的ThreadLocalMap中。ThreadLocalMap又不是普通的HashMap而是一个基于数组和开放地址法实现的定制结构它通过特殊的 hash 递增策略降低冲突并在get、set、remove过程中顺带清理脏Entry。同时由于Entry的key是弱引用、value是强引用因此如果不及时remove()在线程长期存活的场景下就可能留下value无法及时释放的问题这也是ThreadLocal使用中最经典的风险点。

相关新闻