当前位置: 首页 > news >正文

[Java 并发编程] ThreadLocal 原理

ThreadLocal 原理

1. ThreadLocal 基础使用

​ ThreadLocal 被称为线程本地变量类,当多线程并发操作线程本地变量时,实际上每个线程操作的是其独立拥有的本地值,可以理解为每个线程分别独立维护自己的副本。这样就规避了线程安全问题,从而达到无锁并发。

​ 先来一个简单的使用示例:

@DatapublicclassUserContextExample{@DatastaticclassUserContext{// 定义一个用户上下文类privatefinalintuserId;privatefinalintsessionId;}// 设置为线程本地变量privatestaticfinalThreadLocal<UserContext>USER_CONTEXT_THREAD_LOCAL=newThreadLocal<>();// 设置当前线程的用户上下文publicstaticvoidsetUserContext(intuserId,intsessionId){USER_CONTEXT_THREAD_LOCAL.set(newUserContext(userId,sessionId));}// 获取当前线程的用户上下文publicstaticUserContextgetUserContext(){returnUSER_CONTEXT_THREAD_LOCAL.get();}// 清除当前线程的用户上下文publicstaticvoidclearUserContext(){USER_CONTEXT_THREAD_LOCAL.remove();}// 模拟 Web 请求publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServicepool=Executors.newFixedThreadPool(5);for(inti=0;i<10;i++){finalintrequestId=i;pool.execute(()->{try{// 模拟从请求中获取用户信息setUserContext(requestId%3+1,requestId);// 拿到线程本地变量UserContextcontext=getUserContext();// 业务逻辑System.out.println(Thread.currentThread().getName()+" - Processing request for user: "+context.getUserId()+", session: "+context.getSessionId()+", requestId: "+requestId);Thread.sleep(100);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{// 必须清理,这一步尤为关键clearUserContext();}});}// 关闭线程池pool.shutdown();if(!pool.awaitTermination(10,TimeUnit.SECONDS)){pool.shutdownNow();}}}

​ 可以看出,每个线程都独立维护了USER_CONTEXT_THREAD_LOCAL的值 ,相当于这样的结构:

2. 应用场景

  1. 线程隔离

    这是 ThreadLocal 的最主要应用场景,常见的有:数据库连接管理,Session 数据管理。

    对于数据库连接来说,一般完成数据库操作后就要将连接关闭,如果连接不是线程独享的,那么当一个线程完成数据库操作后就不能直接关闭连接,因为尚可能有其他线程连接着该数据库。

  2. 跨函数传递数据

    一个线程设置 ThreadLocal 之后,对于这个线程的任何方法来说,都可以直接获取到其值,而无需通过方法参数传递。这通常适用于一些需要在函数之间频繁传输的数据。

3. ThreadLocal 原理

​ ThreadLocal 中使用了一个重要的数据结构用以维护众多线程的本地变量,称为 ThreadLocalMap。这个数据结构和 HashMap 的区别是,它使用了开放寻址法,而非 HashMap 的链地址法,并且它节点中的 key 均为弱引用包装过的,这个很重要,后面会说到。

​ ThreadLocal 提供的主要 API 其实都是在操作 ThreadLocalMap。其结构如下所示:

​ 每个 Thread 实例拥有一个 Map 实例,每个 Map 实例中有许多 ThreadLocal 实例作为 key,对应的 val 为该 Map 所属 Thread 独立维护的版本。

​ 从逻辑上讲,ThreadLocalMap 应当属于 Thread,但在代码层面 ThreadLocalMap 是作为静态内部类存在于 ThreadLocal 中,这容易让人误以为 ThreadLocalMap 属于 ThreadLocal。这其实是历史遗留问题,在早期的 JDK 版本中,ThreadLocalMap 的确是属于 ThreadLocal 的,也就是每个 ThreadLocal 实例都持有一个 ThreadLocalMap 实例,Map 里面以线程为 key,对应的 val 自然就是该线程维护的版本。这种方案的问题在于,在大部分的应用中,往往线程数是 ThreadLocal 实例数的十倍甚至百倍,如果以线程作为 key,Map 可能需要经常扩容,这样效率就比较低了。因此 JDK8 开始,已经将 ThreadLocalMap 在逻辑上归给 Thread,作为 Thread 的属性存在:ThreadLocal.ThreadLocalMap threadLocals;,不过 ThreadLocalMap 的源码依然存在于 ThreadLocal 类。

​ ThreadLocalMap 的节点使用弱引用进行了包装:

staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}

​ 这个弱引用是什么意思呢?就拿刚刚的USER_CONTEXT_THREAD_LOCAL为例,我们知道这是一个引用,而且是强引用,引用的实例就是new ThreadLocal<>(),只要这个引用还存在,实例就不会被 GC 回收。ThreadLocalMap 的 key 也是一个引用,但它是被WeakReference类包装的。规则是,如果一个实例仅存在弱引用,下一次 GC 就会回收它。引用我们可以理解为一种对实例的追踪方式,弱引用就是一类不会影响 GC 的追踪方式。

privatestaticvoidrefTest(){ObjectstrongRef=newObject();// 强引用WeakReference<Object>weakRef=newWeakReference<>(strongRef);// 弱引用System.gc();System.out.println(strongRef);// java.lang.Object@46f7f36aSystem.out.println(weakRef.get());// java.lang.Object@46f7f36astrongRef=null;System.gc();System.out.println(weakRef.get());// null}

​ 因此,如果这样写USER_CONTEXT_THREAD_LOCAL = null,那么实例就会被回收了。但事实上我们是没办法这样写的,因为已经将其设为 final 了,不能更改了。

​ 需要注意的是,若实例被回收,entry 的 key 变为 null 之后,value 仍然强引用在 entry 中,当后续调用setgetremove这些方法时,在方法内部才会触发这些 key 为 null 的 entry 的清理,也就是惰性清理的模式。因此,如果线程一直不终止(例如线程池中的线程),并且没有调用 ThreadLocal 的setgetremove来触发清理,value 会一直存在,造成 value 的内存泄漏。

​ ThreadLocal 在规范上要设为 static final,因为从语义上来说,ThreadLocal 本身并不存储数据,而是作为键来访问每个线程的 ThreadLocalMap 中的值。一个 ThreadLocal 实例应该对应于一个特定类型的线程局部变量,这个对应关系是全局唯一且不变的,因此用 static 保证一个特定类型的 ThreadLocal 的全局唯一性。final 是为了不使外部修改其引用,一旦引用被修改,如USER_CONTEXT_THREAD_LOCAL = new ThreadLocal<>(),那么原来的实例由于没有强引用了,就会被回收,进而 ThreadLocalMap 中原来指向旧实例的 key 指向 null,进而无法访问原先的 val,造成数据丢失。

​ 这里就有点矛盾,将 ThreadLocal 设为 final 会导致其永远存在强引用,ThreadLocal 实例就永远不会自动释放,key 就永远不指向 null,val 就永远不被清理。看了半天,弱引用也用不上啊。其实本来这个弱引用也只是一种防御性手段,始终记住在使用完一个线程本地变量后调用 remove 手动删除才是正经。

4. 结语

​ ThreadLocal 本质上还是空间换时间的思想,每个线程修改自己的副本,从而无锁并发执行。

http://www.jsqmd.com/news/210979/

相关文章:

  • 炸裂汇总!2025收官硬核干货:380+页深度拆解RAG/Agent/MCP等9大核心,建议熬夜研读!
  • 开发了一个免费的批量视频语音字幕识别工具,核心点是可批量自动处理识别任务
  • Java小白求职者在互联网大厂面试:从Spring Boot到微服务的技术探索
  • 重塑未来安全格局的五大前沿技术:从AI安全到零信任的深度解读
  • 【2026年最新】有关漏洞挖掘的一些总结,新手小白网络安全入门必看的经验教训!
  • 边缘模型增量微调实战
  • 新中地学员转行学GIS开发原因盘点①
  • 构建企业级安全防线:盘点网络安全防范的核心技术及其实战应用体系
  • FileImgSwap 文图变文件藏到图片是一款可以把文件与 PNG 图像进行互转的工具
  • 2026最新流出!6款免费AI写论文工具,1天5万字还带真实参考文献!
  • 全网最全8个AI论文工具,本科生轻松搞定论文格式!
  • 学术生产力七重奏:当Paperzz领衔六大AI写作引擎,毕业论文从“卡壳”走向“丝滑交付”
  • ‌高并发系统测试案例解析
  • 图片格式转换与尺寸批量大师 支持ICO PNG JPG等八种格式 批量转换与自定义尺寸
  • Python与USB 3.0用户态设备驱动:技术挑战与创新实践
  • 2025年新中地转行数据:谁才是GIS开发的主力军专业?
  • 收藏!AI工程师分2派?一文分清传统算法与大模型应用,小白转行必看
  • Loomis Sayles隆重庆祝百年华诞,矢志不渝服务客户、铸就投资卓越
  • 文件夹及文件目录提取器 - 高效管理支持按指定层级提取目录结构提取深度提取文件目录结构的专业工具
  • 电脑挂机锁是一款专为需要电脑挂机场景打造的(防偷窥电脑屏幕)隐私保护实用工具
  • 零日漏洞黑市:一个价值百万美元的地下生态系统
  • 测试人员心理安全:勇于报错
  • 自考必备10个降AI率工具,高效降AIGC不踩坑
  • 虚拟战场:深度解析20款安全软件对抗10大新兴威胁的真实效能
  • 体积仅3M大小,批量文件重命名神器,支持拖拽改名,完全免费绿色无广告使用!zRenamer绿色版
  • 电脑GIF动图录制软件是一款轻量、专注、高效的动图制作小工具(独家原创小工具界面很漂亮)
  • git cherry-pick使用
  • 超全 PDF 多功能处理工具箱,格式转换、页面提取,页面删除,压缩批量重命名等样样行,实用软件
  • IDA Pro 逆向入门:转行二进制安全的必备工具使用指南
  • 1G视频,一键压缩到200M!免费且强大的小丸工具箱,附带免安装版本和安装版,视频压缩神器