news 2026/3/28 4:40:06

[Java 并发编程] ThreadLocal 原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[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 本质上还是空间换时间的思想,每个线程修改自己的副本,从而无锁并发执行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/21 9:15:54

1G视频,一键压缩到200M!免费且强大的小丸工具箱,附带免安装版本和安装版,视频压缩神器

下载链接 https://tool.nineya.com/s/1jbp0rqrg 软件介绍 小丸工具箱是一款功能强大、界面简洁&#xff0c;用于处理音视频等多媒体文件的软件。小丸工具箱是一款基于x264、ffmpeg等命令行程序的图形界面&#xff0c;它的目标是让视频压制变得简单、轻松。它支持字幕批量压制…

作者头像 李华
网站建设 2026/3/26 19:32:01

行锁真的解决了可重复读下的幻读问题吗?

关于 RR 级别下的幻读&#xff0c;其实大部分场景都被 MVCC 和 Next-Key Lock 解决了。但在一种特殊情况下&#xff0c;幻读依然存在。 首先要知道只快照读的话只靠MVCC就能防止快读。涉及到当前读加锁就能避免&#xff0c;但是下面这种情况是先快照读&#xff0c;再当前读导致…

作者头像 李华
网站建设 2026/3/17 3:39:38

EasyGBS:融合算法与算力的核心能力及行业应用价值

在数字化、信息化快速发展的今天&#xff0c;视频监控已经成为各行各业不可或缺的一部分。从个人用户到大型企业&#xff0c;再到政府部门&#xff0c;视频监控的需求无处不在。在众多视频监控平台中&#xff0c;国标GB28181算法算力平台EasyGBS凭借其独特的优势&#xff0c;脱…

作者头像 李华
网站建设 2026/3/16 11:55:12

linux服务-MariaDB 10.6 Galera Cluster 部署

MariaDB 10.6 Galera Cluster 部署 文档参考&#xff1a;MariaDB Galera Cluster 10.6 集群部署&#xff0c; 豆包ai MariaDB 10.6 Galera Cluster是基于MariaDB 10.6社区版与Galera 4同步复制技术深度整合的高可用、多主复制集群解决方案&#xff0c;专为解决传统异步复制的…

作者头像 李华
网站建设 2026/3/24 16:31:18

新手入门:Web安全测试大盘点

随着互联网时代的蓬勃发展&#xff0c;基于Web环境下的应用系统、应用软件也得到了越来越广泛的使用。 目前&#xff0c;很多企业的业务发展都依赖于互联网&#xff0c;比如&#xff0c;网上银行、网络购物、网络游戏等。但&#xff0c;由于很多恶意攻击者想通过截获他人信息去…

作者头像 李华
网站建设 2026/3/26 11:21:16

基于单片机的视觉导航小车设计

2 项目硬件设计 2.1 主控模块设计 2.1.1单片机选型 控制芯片的选择应适合设计要求&#xff0c;性能应具有功耗低&#xff0c;性能高&#xff0c;稳定性好&#xff0c;存储空间大的优点。当设计要求准确时&#xff0c;代码指令的兼容性就很高。 STM32结构设计最大限度地减少了外…

作者头像 李华