news 2026/4/18 6:25:39

[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/4/18 6:24:31

Kubernetes环境下的性能测试新范式

在云原生时代&#xff0c;Kubernetes&#xff08;K8s&#xff09;已成为容器编排的事实标准&#xff0c;它通过自动化部署、扩展和管理&#xff0c;彻底改变了应用架构。然而&#xff0c;这种动态环境对性能测试提出了独特挑战&#xff1a;容器化应用的弹性伸缩、微服务间网络延…

作者头像 李华
网站建设 2026/4/5 19:56:09

基于单片机的蓝牙无线密码锁设计

2 系统硬件设计 2.1 设计原理 本设计的主要硬件由单片机[5]、显示模块、驱动模块等硬件组成。在整个系统运转时&#xff0c;单片机会依照用户实际输入的对应内容&#xff0c;在此过程中&#xff0c;单片机判断用户输入密码的正确性。如果成功的输入正确的密码&#xff0c;继电器…

作者头像 李华
网站建设 2026/4/16 2:09:56

工业通信中的“工业战狼”!耐达讯自动化CAN转PROFIBUS网关

在某污水处理厂的配电室内&#xff0c;值班工程师小李正面对着24台智能电表的异常报警。这些设备虽具备智能采集功能&#xff0c;却因协议壁垒导致SCADA系统频繁报错&#xff0c;每次故障排查需耗费3小时。这正是传统工业现场的典型困境——当电力仪表的"方言"无法被…

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

突破传统点式测量:DIC技术在复合材料全场变形与损伤表征中的应用

前言&#xff1a;数字图像相关&#xff08;DIC&#xff09;技术作为一种实用且有效的物体表面变形测量工具&#xff0c;目前已被广泛应用于测量复合材料的变形和损伤行为。复合材料具有非均匀性和各向异性的性质&#xff0c;导致其受载后会产生复杂的变形行为。DIC技术具有全场…

作者头像 李华
网站建设 2026/4/17 21:14:23

如何用 4 种可靠的方法更换 iPhone

Apple 每年都会发布新版本的 iPhone。升级到新 iPhone 是一种令人兴奋的体验&#xff0c;但转移所有宝贵数据的想法有时会让人感到畏惧。幸运的是&#xff0c;我们准备了 4 种有效的更换 iPhone 的方法&#xff0c;让你可以毫不费力地更换到你的新 iPhone。此外&#xff0c;您还…

作者头像 李华