并发编程 - ThreadLocal 线程本地变量

news/2026/1/21 16:15:49/文章来源:https://www.cnblogs.com/liushoushou/p/19512374

知识点 12:并发编程 —— ThreadLocal 线程本地变量

1. 是什么?它解决了什么问题?

ThreadLocal 是 Java 提供的一个非常独特的解决线程安全问题的工具,它提供了一种全新的思路:不共享,即安全

它的核心思想是:为每一个使用该变量的线程都提供一个独立的、私有的变量副本。每个线程都只能读写自己的副本,而不能访问其他线程的副本。这样,每个线程操作的都是自己的“私有”变量,自然就不存在线程安全问题。

ThreadLocal 解决了什么问题?

它提供了一种“以空间换时间”的方式来保证线程安全,避免了传统锁机制(如 synchronized)带来的性能开销和复杂性。在以下场景中非常有用:

  1. 管理线程不安全的工具类实例:例如 SimpleDateFormatRandom 等不是线程安全的类。通过 ThreadLocal,每个线程可以持有自己的实例,避免了频繁创建对象或对共享实例加锁。
  2. 传递上下文信息:在复杂的业务调用链中,用来传递贯穿整个请求的上下文信息,例如用户身份信息、事务 ID、数据库连接等。这样就避免了将这些信息作为方法参数层层传递,使代码更简洁、可读性更高。
  3. 避免共享资源的竞争:当多个线程需要访问同一个资源,但又不希望它们相互影响时,ThreadLocal 可以为每个线程提供独立的资源副本。

生活比喻ThreadLocal 就像是去银行办理业务时,银行发给你的专属储物柜的钥匙

  • ThreadLocal 变量:就是那个储物柜本身。它对所有来银行的人(线程)都是可见的。
  • set(value):你(当前线程)用你的钥匙,把你自己的私人物品(比如 User 对象)存入你的专属储物柜里。
  • get():你随时可以用你的钥匙,从你的储物柜里取出你自己的物品。你无法打开别人的储物柜,别人也无法打开你的。
  • 线程销毁:当你离开银行(线程销毁)时,你的储物柜和里面的东西理论上就应该被清理掉。

2. 实现原理是怎样的?

很多人误以为 ThreadLocal 内部有一个 Map<Thread, Object> 来存储每个线程的值。但实际上,它的设计更巧妙。

核心思想每个 Thread 对象内部都维护着一个 ThreadLocalMap 类型的成员变量,这个 Map 才是真正存储线程本地数据的地方。

  • ThreadThreadLocalMap
    • 每个线程 Thread 内部都有一个成员变量,名为 threadLocals,它的类型是 ThreadLocal.ThreadLocalMap。这个 ThreadLocalMap 是懒加载的,只在线程第一次使用 ThreadLocal 时创建。
  • ThreadLocalMap 的结构
    • 这是一个 ThreadLocal 的内部静态类,类似于一个简化的 HashMap,它内部维护着一个 Entry[] 数组。
    • 每个 Entry 对象都继承自 WeakReference<ThreadLocal<?>>。这意味着 EntryKey 是 ThreadLocal 对象本身,并且是一个弱引用
    • EntryValue 才是我们想要存储的线程本地变量值,并且是一个强引用

set(value)get() 的核心流程

  1. set(value) 流程

    1. 获取当前线程 Thread.currentThread()
    2. 从当前线程对象中,获取它的成员变量 threadLocals (即 ThreadLocalMap)。
    3. 如果这个 map 不存在,就为当前线程创建一个新的 ThreadLocalMap
    4. 当前的 ThreadLocal 对象作为 Key(被 WeakReference 包装),要存的 value 作为 Value,存入这个 ThreadLocalMap 中。
  2. get() 流程

    1. 获取当前线程
    2. 获取该线程的 threadLocals map。
    3. 如果 map 存在,就以当前的 ThreadLocal 对象为 Key,从 map 中取出对应的 Value 并返回。
    4. 如果 map 或对应的 Entry 不存在,则会进行初始化并返回 null(或者 initialValue() 方法的返回值)。

总结:数据是存储在线程自己内部的 ThreadLocalMap 里的,而不是 ThreadLocal 对象里。ThreadLocal 对象本身仅仅是一个“索引”或者“钥匙”,用于从当前线程的 ThreadLocalMap 中找到并操作对应的线程本地值。


3. 内存泄漏问题及如何避免

这是 ThreadLocal 最重要的考点,也是使用时最容易出错的地方。

问题根源ThreadLocalMap 中的 Key 是弱引用,而 Value 是强引用

  • 弱引用的 Key ( ThreadLocal 对象 ):当外部(例如业务代码)不再有任何强引用指向一个 ThreadLocal 对象时,这个 ThreadLocal 对象就会在下次垃圾回收时被回收。此时,ThreadLocalMap 中对应的 Entry 的 Key 就会变成 null
  • 强引用的 Value:然而,这个 Key 为 nullEntry 中存储的 Value(即我们存入的数据),仍然是一个强引用,它不会被自动回收。

内存泄漏的发生
如果线程是一个长生命周期的线程(例如线程池中的线程),它会一直存在,那么它的 ThreadLocalMap 也会一直存在。此时,即使 ThreadLocal 对象被回收,但 Key 变为 nullEntry 中的 Value 却无法被回收,导致这部分内存一直被占用,从而引发内存泄漏

ThreadLocalMap 内部的补偿清理机制

ThreadLocalMap的设计者预见了 Key 被回收后 Value 无法被访问的问题,并内置了一些补偿性的清理机制

  • 清理时机:当调用ThreadLocalget(), set(), remove()方法时,ThreadLocalMap顺便检查并清理那些key已经为null的“脏”Entry
  • 工作方式
    1. get()set()操作过程中,当遍历Entry数组时,如果发现某个Entrykeynull,它会触发一次启发式清理(expungeStaleEntry),清除这个Entry及其附近的其他“脏”Entry
    2. remove()方法在移除当前ThreadLocal对应的Entry后,也会触发一次清理。

结论ThreadLocalMap确实有被动的、补偿性的清理机制。但是,我们绝对不能依赖它来避免内存泄露,因为这些清理操作的触发时机是不确定的。如果一个“脏”Entry一直不被get, set等操作触及,它就可能永远得不到清理。

如何避免内存泄漏?

为了彻底避免内存泄漏,最佳实践是:在每次使用完 ThreadLocal 后,务必在 finally 块中调用 threadLocal.remove() 方法。

  • remove() 方法的作用:它会从当前线程的 ThreadLocalMap 中移除对应的 Entry(包括 Key 和 Value)。这样,Key 为 nullEntry 及其对应的 Value 都会被清除,从而允许垃圾收集器正常回收这部分内存。
  • 最佳实践场景:特别是在线程池环境中,ThreadLocalremove() 操作是强制性的,必须在任务结束时清理。
  • Spring 框架的处理:在 Spring Framework 中,对于请求范围(request scope)或会话范围(session scope)的 Bean,以及事务管理等场景,Spring 内部会妥善处理 ThreadLocalremove() 操作(通常在请求结束或事务提交/回滚时),因此开发者通常无需手动干预。但在自定义的非 Spring 管理的线程或 ThreadLocal 使用中,手动 remove() 是强制性的。
// 示例:如何正确使用 ThreadLocal 并避免内存泄漏
ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
try {userThreadLocal.set("some_user_context");// ... 执行业务逻辑 ...
} finally {// 务必在 finally 块中调用 remove(),确保在任何情况下都能清理userThreadLocal.remove();
}

内存泄露的危害场景分析

  1. 危害小的场景

    • 线程生命周期短:当一个线程执行完任务就销毁时,它所持有的ThreadLocalMap也会随之被销毁。即使在运行期间产生了“脏”Entry,随着线程的死亡,整个ThreadLocalMap(包括里面的强引用value)都会被垃圾回收。
    • 例子:为每个任务创建一个新线程,执行完就结束,这种情况下即使忘记remove(),影响也有限。
  2. 危害大的场景

    • 线程池(Thread Pool):这是ThreadLocal内存泄露最典型、最危险的场景!
    • 原因:线程池中的线程是被复用的,它们的生命周期非常长,几乎和应用程序一样长。当一个任务结束时,处理该任务的线程并不会销毁,而是被归还给线程池,等待下一个任务。
    • 后果:如果在任务代码中使用了ThreadLocal但忘记remove(),那么这个ThreadLocalvalue就会一直被这个存活的、被复用的线程所持有,永远无法被回收。随着越来越多的任务被这个线程执行,ThreadLocalMap中积累的“脏”Entry会越来越多,最终导致OOM(内存溢出)

4. 核心代码示例:数据库连接管理

ThreadLocal 的一个经典应用场景,就是在同一个线程的多次数据库操作中,共享同一个 Connection 对象,避免频繁创建和关闭连接。

package com.study.concurrency;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;class ConnectionManager {// 1. 创建一个 ThreadLocal 对象来存储 Connectionprivate static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();public static Connection getConnection() {// 2. 先从当前线程的 ThreadLocal 中获取连接Connection conn = connectionHolder.get();try {// 如果没有连接,或者连接已关闭if (conn == null || (conn.isClosed() && conn != null)) { // 增加 conn != null 避免 NPE// 3. 创建一个新的连接// 注意:这里只是示例,实际生产环境应使用连接池conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");// 4. 将新连接存入当前线程的 ThreadLocalconnectionHolder.set(conn);System.out.println(Thread.currentThread().getName() + ": 创建了一个新的数据库连接。");}} catch (SQLException e) {e.printStackTrace();}return conn;}public static void closeConnection() {Connection conn = connectionHolder.get();try {if (conn != null && !conn.isClosed()) {conn.close();System.out.println(Thread.currentThread().getName() + ": 关闭了数据库连接。");}} catch (SQLException e) {e.printStackTrace();} finally {// 5. 务必调用 remove(),防止内存泄漏connectionHolder.remove();System.out.println(Thread.currentThread().getName() + ": 清理了 ThreadLocal。");}}
}public class ThreadLocalDemo {public static void main(String[] args) throws InterruptedException {Runnable task = () -> {System.out.println(Thread.currentThread().getName() + ": 开始执行任务。");// 在同一个线程中,第一次获取连接会创建新的Connection conn1 = ConnectionManager.getConnection();// 第二次获取,会直接复用第一次的连接Connection conn2 = ConnectionManager.getConnection();System.out.println(Thread.currentThread().getName() + ": conn1 和 conn2 是同一个对象吗? " + (conn1 == conn2));// 模拟业务操作try {Thread.sleep((long) (Math.random() * 100));} catch (InterruptedException e) {Thread.currentThread().interrupt();}// 线程结束前,关闭连接并清理 ThreadLocalConnectionManager.closeConnection();System.out.println(Thread.currentThread().getName() + ": 任务执行结束。");};// 两个线程会各自创建和使用自己的连接,互不干扰Thread t1 = new Thread(task, "线程A");Thread t2 = new Thread(task, "线程B");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1195380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI绘画趋势一文详解:Z-Image-Turbo等开源模型部署方式演进

AI绘画趋势一文详解&#xff1a;Z-Image-Turbo等开源模型部署方式演进 你有没有想过&#xff0c;只需要几行命令和一个浏览器&#xff0c;就能在本地运行一个强大的AI绘画工具&#xff1f;如今&#xff0c;像 Z-Image-Turbo 这样的开源图像生成模型正在让这一切变得轻而易举。…

Live Avatar新手必看:首次运行常见问题解决指南

Live Avatar新手必看&#xff1a;首次运行常见问题解决指南 1. 引言&#xff1a;快速上手前的必要准备 你刚下载了Live Avatar这个由阿里联合高校开源的数字人项目&#xff0c;满心期待地想要生成一个属于自己的虚拟形象视频。但一运行就遇到显存不足、进程卡死、NCCL报错等问…

并发请求支持多少?API吞吐量基准部署教程

并发请求支持多少&#xff1f;API吞吐量基准部署教程 1. 功能概述 本工具基于阿里达摩院 ModelScope 的 DCT-Net 模型&#xff0c;支持将真人照片转换为卡通风格。 支持的功能&#xff1a; 单张图片卡通化转换批量多张图片处理多种风格选择&#xff08;当前支持标准卡通风格…

吐血推荐!专科生毕业论文必备的10个AI论文平台

吐血推荐&#xff01;专科生毕业论文必备的10个AI论文平台 2026年专科生论文写作工具测评&#xff1a;为什么你需要一份精准指南 随着AI技术在学术领域的深入应用&#xff0c;越来越多的专科生开始依赖智能写作工具来提升论文效率与质量。然而&#xff0c;面对市场上五花八门的…

国外期刊怎么找:实用查找方法与途径指南

刚开始做科研的时候&#xff0c;我一直以为&#xff1a; 文献检索就是在知网、Google Scholar 里反复换关键词。 直到后来才意识到&#xff0c;真正消耗精力的不是“搜不到”&#xff0c;而是—— 你根本不知道最近这个领域发生了什么。 生成式 AI 出现之后&#xff0c;学术检…

【稀缺技术曝光】:仅需3步,用MCP协议赋予AI Agent系统级文件控制能力

第一章&#xff1a;MCP协议与AI Agent融合的革命性意义 在人工智能技术飞速发展的背景下&#xff0c;MCP&#xff08;Multi-agent Communication Protocol&#xff09;协议与AI Agent的深度融合正引发一场技术范式的变革。这一融合不仅提升了智能体之间的协同效率&#xff0c;更…

Dify部署后上传不了文件?90%的人都忽略了这个关键配置!

第一章&#xff1a;Dify部署后上传文件提示 413 Request Entity Too Large 在完成 Dify 的本地或服务器部署后&#xff0c;用户在尝试上传较大文件时可能会遇到 413 Request Entity Too Large 错误。该问题通常并非由 Dify 应用本身引起&#xff0c;而是其前置代理服务&#x…

SVPWM_Inverter_Inductor_Motor:基于MATLAB/Simulink...

SVPWM_Inverter_Inductor_Motor&#xff1a;基于MATLAB/Simulink的空间矢量脉宽调制SVPWM逆变器&#xff0c;交流测连接三相感应电机。 仿真条件&#xff1a;MATLAB/Simulink R2015b空间矢量脉宽调制&#xff08;SVPWM&#xff09;这玩意儿在电机控制里算是经典操作了&#xff…

“天下工厂”是否支持定制化的相关功能?

现阶段&#xff0c;“天下工厂”平台主要是把为制造业B2B用户提供高效、精准且标准化的工厂与老板查询服务作为核心定位&#xff0c;它在产品设计方面着重突出了三大核心能力&#xff0c;具体如下&#xff1a;能够做到百分之百分辨出真实的生产企业和经销商&#xff1b;可以精准…

如何通过GNSS位移监测提升单北斗变形监测系统的精度与应用效果?

本文以GNSS技术在单北斗变形监测系统中的应用为核心&#xff0c;探讨如何提升其精度与效果。研究涉及单北斗GNSS在地质灾害监测和桥梁形变监测等领域的实际案例&#xff0c;分析其有效性与先进性。同时&#xff0c;重点介绍了系统的安装与维护要点&#xff0c;确保技术稳定运行…

Unsloth加速比实测:不同模型训练时间对比表

Unsloth加速比实测&#xff1a;不同模型训练时间对比表 Unsloth 是一个专注于提升大语言模型&#xff08;LLM&#xff09;微调效率的开源框架&#xff0c;支持高效、低显存的模型训练与部署。它通过内核融合、梯度检查点优化和自定义 CUDA 内核等技术&#xff0c;在保持训练精…

winform 窗体关闭原因的枚举类型

枚举值解释:None - 无特定原因默认值,表示没有明确的关闭原因或原因未知WindowsShutDown - Windows系统关闭当操作系统正在关机或重启时触发这是系统级事件,应用程序通常需要保存数据并快速响应MdiFormClosing - MD…

Glyph能否处理PDF?文档图像化解析实战教程

Glyph能否处理PDF&#xff1f;文档图像化解析实战教程 1. Glyph&#xff1a;用视觉推理突破文本长度限制 你有没有遇到过这样的情况&#xff1a;手头有一份上百页的PDF报告&#xff0c;想让大模型帮你总结重点&#xff0c;结果发现大多数AI根本“读不完”这么长的内容&#x…

麦橘超然社交媒体运营:爆款图文内容生成实战

麦橘超然社交媒体运营&#xff1a;爆款图文内容生成实战 1. 引言&#xff1a;为什么AI图像正在改变社交媒体游戏 你有没有发现&#xff0c;最近朋友圈、小红书、抖音上的配图越来越“电影感”&#xff1f;那种光影细腻、构图惊艳、一看就忍不住点赞的图片&#xff0c;很多已经…

2026低代码开发平台排行榜:国内外主流平台全景解析与选型指南

请原谅我今天&#xff0c;冒昧地拉着你聊低代码——这个在IT圈火了好几年&#xff0c;却依然有人摸不透的话题。 “低代码”这个词&#xff0c;是我从业十多年来&#xff0c;看着从冷门工具长成行业风口的存在。 为什么以前不敢深聊&#xff1f;因为误解太多。 有人觉得它是“…

uipath-windows禁用更新任务

背景&#xff1a;在windows xp电脑上安装了ui path 2021.4.4版本的&#xff0c;然后想关闭版本更新&#xff0c;本来也是想用下边的禁用更新任务流程的方法来禁止版本更新的&#xff0c;然后发现开始没找到ui path的更新任务&#xff0c;后来自动升级到2021.10.3版本的之后了&a…

从Vue3到Spring Boot:一位Java全栈开发者的实战面试记录

从Vue3到Spring Boot&#xff1a;一位Java全栈开发者的实战面试记录 面试场景回顾 今天&#xff0c;我参加了一场针对Java全栈开发工程师的面试。面试官是一位经验丰富的技术负责人&#xff0c;而我则是一个拥有5年工作经验的开发者&#xff0c;目前在一家互联网大厂担任核心…

并发 - AQS 与 Volatile

知识点 5.5:并发编程基石 —— AQS 与 Volatile 在深入了解各种锁和同步工具之前,必须先理解 JUC 框架的两个核心基石:volatile 关键字和 AQS 框架。1. volatile 关键字:并发编程的“信号旗” volatile 是一个 Jav…

上海AI公司推荐哪家好?权威推荐六家头部AI企业!

在人工智能逐渐从“技术突破期”迈向“产业兑现期”的背景下,市场对AI公司的评价标准正在发生根本性变化。单一算法能力、模型参数规模,已不足以支撑长期竞争力,系统能力、工程化水平与行业适配深度正成为核心判断依…

【Dify高级用法】:Iteration节点处理数组数据的3种高阶模式

第一章&#xff1a;Iteration节点的核心机制与数据流原理 基本概念与运行模型 Iteration节点是工作流系统中用于实现循环处理的关键组件&#xff0c;其核心功能是在满足特定条件时重复执行一组操作。该节点通过接收输入数据流&#xff0c;并在每次迭代中处理一个元素&#xff…