在 Java 多线程编程中,ThreadThreadLocalThreadLocalMap 是三个紧密相关的类,它们共同构成了 Java 中**线程本地变量(Thread-Local Storage)**机制的基础。下面我将从 三者的关系实现原理 以及 实际开发中的应用 三个方面进行详细讲解。


🧱 一、三者的关系(结构图)

Thread└── ThreadLocal.ThreadLocalMap threadLocals(线程私有)└── Entry[] table(键值对数组)└── Entry extends WeakReference<ThreadLocal<?>>├── key: ThreadLocal 实例(弱引用)└── value: 线程本地变量值

1. Thread 类

  • 每个线程(Thread)都有一个私有的属性:
    ThreadLocal.ThreadLocalMap threadLocals;
  • 这个属性保存了该线程所有与 ThreadLocal 相关的变量。

2. ThreadLocal<T> 类

  • 提供线程本地变量的访问接口。
  • 每个线程通过 ThreadLocal 实例访问自己的本地变量。
  • 常用方法:
    java

    深色版本

    void set(T value);   // 设置线程本地变量
    T get();             // 获取线程本地变量
    void remove();       // 移除线程本地变量

3. ThreadLocalMap 类

  • 是 ThreadLocal 的一个静态内部类。
  • 本质上是一个自定义的 Map 结构,用于存储线程本地变量。
  • 键(Key)是 ThreadLocal 实例(弱引用),值(Value)是线程本地值
  • 与 HashMap 不同,它不是线性探测,而是开放寻址法(open addressing)。

🔍 二、实现原理详解

1. 数据结构

  • 每个线程都有自己的 ThreadLocalMap,它保存了所有与该线程绑定的 ThreadLocal 变量。
  • ThreadLocalMap 中的 Entry 是一个 WeakReference<ThreadLocal<?>>,它的 key 是弱引用,value 是强引用。

2. ThreadLocal.set() 的过程

  1. 获取当前线程 t = Thread.currentThread()
  2. 获取线程的 ThreadLocalMap t.threadLocals
  3. 如果存在,则设置 map.set(this, value)
  4. 如果不存在,则创建一个新的 ThreadLocalMap 并设置初始值

3. ThreadLocal.get() 的过程

  1. 获取当前线程 t = Thread.currentThread()
  2. 获取线程的 ThreadLocalMap
  3. 如果存在,查找以 this 为 key 的值并返回
  4. 如果不存在或找不到值,调用 initialValue() 初始化一个默认值(默认为 null)

🧼 三、为什么使用弱引用(WeakReference)?

  • ThreadLocalMap 的 key 是 ThreadLocal 的弱引用,这样做的目的是为了防止内存泄漏。
  • 如果 key 是强引用,当 ThreadLocal 实例不再被外部引用时,由于线程还持有它的引用,GC 无法回收,导致内存泄漏。
  • 使用弱引用可以让 ThreadLocal 在没有外部引用时被回收,但需要注意 value 仍然可能未被清除(需要手动调用 remove())。

📌 四、实际开发中的应用场景

✅ 1. 用户上下文传递(如登录信息)

public class UserContext {private static final ThreadLocal<String> currentUser = new ThreadLocal<>();public static void setCurrentUser(String user) {currentUser.set(user);}public static String getCurrentUser() {return currentUser.get();}public static void clear() {currentUser.remove();}
}

使用场景:

  • 在 Web 应用中,每个请求由一个线程处理,可以将当前用户信息存入 ThreadLocal,避免层层传递。
  • 在 AOP、拦截器中设置,业务代码中直接获取当前用户。

✅ 2. 数据库事务管理

public class TransactionManager {private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();public static void setConnection(Connection conn) {connectionHolder.set(conn);}public static Connection getConnection() {return connectionHolder.get();}public static void clear() {connectionHolder.remove();}
}

使用场景:

  • 同一线程内多个 DAO 方法共享同一个事务连接。
  • 避免传递 Connection 参数,实现事务一致性。

✅ 3. 日志追踪 ID(Trace ID)

public class TraceContext {private static final ThreadLocal<String> traceId = new ThreadLocal<>();public static void setTraceId(String id) {traceId.set(id);}public static String getTraceId() {return traceId.get();}public static void clear() {traceId.remove();}
}

使用场景:

  • 在分布式系统中,为每个请求分配一个唯一 Trace ID,记录在 ThreadLocal 中,方便日志追踪。
  • 日志框架(如 Logback、Log4j)可以集成该机制,自动打印 Trace ID。

✅ 4. 避免线程安全问题(替代 synchronized)

某些场景下,可以通过 ThreadLocal 替代加锁,提升性能。

例如,SimpleDateFormat 是线程不安全的类,可以这样使用:

private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));// 使用
String dateStr = sdf.get().format(new Date());

⚠️ 五、使用注意事项(避免内存泄漏)

问题建议
不及时调用 remove()导致 value 无法回收,造成内存泄漏
线程池中使用 ThreadLocal线程复用导致旧值残留,需在任务前后手动清理
使用弱引用的 keykey 会被回收,但 value 仍存在,需配合清理机制

✅ 线程池中使用 ThreadLocal 的正确方式:

ExecutorService executor = Executors.newFixedThreadPool(5);Runnable task = () -> {try {UserContext.setCurrentUser("user123");// 执行业务逻辑...} finally {UserContext.clear(); // 必须手动清理}
};executor.submit(task);

📊 六、总结:三者关系一览表

名称类型作用
Thread每个线程对象,包含一个 ThreadLocalMap
ThreadLocal提供线程本地变量的访问接口
ThreadLocalMap类(ThreadLocal 的静态内部类)实际存储线程本地变量的 Map,键为 ThreadLocal,值为变量值

🧩 七、延伸:InheritableThreadLocal

InheritableThreadLocalThreadLocal 的子类,用于实现父子线程之间的本地变量继承

ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

使用场景:

  • 创建子线程时继承父线程的本地变量(如日志上下文、用户信息等)。

📚 八、推荐工具和库

工具/库用途
TransmittableThreadLocal(TTL)阿里开源,解决线程池中 ThreadLocal 传递问题
MDC(Mapped Diagnostic Context)Logback/Log4j 中的日志上下文追踪,底层使用 ThreadLocal
ThreadLocal + AOP实现自动上下文管理,如日志、事务等

为什么 用户上下文传递(如登录信息) 不需要 创建ThreadLocalMap ?

在讨论 ThreadLocalThreadThreadLocalMap 三者的关系时,理解它们如何协同工作对于掌握 Java 中线程本地存储(Thread-Local Storage)的概念至关重要。当你使用 ThreadLocal 来实现用户上下文传递(如登录信息)时,实际上并不需要你手动创建 ThreadLocalMap,这是因为 ThreadLocal 的内部机制已经为你处理了这个过程。

为什么不需要手动创建 ThreadLocalMap

  1. 自动管理:当你通过 ThreadLocalset() 方法设置一个值时,如果当前线程的 ThreadLocalMap 尚未初始化(即首次调用 set()),ThreadLocal 会自动为该线程创建一个新的 ThreadLocalMap 并将键值对插入其中。同样的,当你调用 get() 方法获取值时,如果没有找到对应的 ThreadLocalMap,它也会自动进行初始化。

  2. 封装性ThreadLocal 类的设计目的是为了提供一种简单的方式来访问线程本地变量,而不需要直接操作底层的数据结构(如 ThreadLocalMap)。因此,所有的复杂性都被封装在了 ThreadLocal 类和它的内部类 ThreadLocalMap 中。

  3. 透明性:开发者只需关注如何使用 ThreadLocal 来存储和检索数据,而无需关心如何管理 ThreadLocalMap。这种设计使得代码更加简洁,易于理解和维护。

示例解释

考虑以下示例,用于说明如何使用 ThreadLocal 实现用户上下文传递:

public class UserContext {private static final ThreadLocal<String> currentUser = new ThreadLocal<>();public static void setCurrentUser(String user) {currentUser.set(user); // 如果没有ThreadLocalMap,则会自动创建}public static String getCurrentUser() {return currentUser.get(); // 自动处理ThreadLocalMap的存在与否}public static void clear() {currentUser.remove(); // 清理资源}
}

在这个例子中,我们定义了一个静态的 ThreadLocal 变量 currentUser 来存储每个线程的用户信息。当我们调用 setCurrentUser() 方法时,ThreadLocal 会检查当前线程是否有 ThreadLocalMap。如果没有,它会自动创建一个并将其与当前线程关联起来。同样地,getCurrentUser() 方法也会自动处理 ThreadLocalMap 的存在情况,确保能够正确地返回当前线程的用户信息。

总结

  • 无需手动创建 ThreadLocalMap:由于 ThreadLocal 的设计目标是简化线程本地变量的使用,因此所有关于 ThreadLocalMap 的管理工作都是自动完成的。
  • 自动化管理:无论是设置值还是获取值,ThreadLocal 都会根据需要自动管理其内部的 ThreadLocalMap
  • 面向接口编程:使用者只需要关心如何使用 ThreadLocal 提供的方法来操作线程本地变量,而不必深入理解其实现细节。

这种方式不仅简化了开发者的任务,还减少了出错的可能性,使得 ThreadLocal 成为了多线程环境下管理线程局部数据的强大工具。

怎么保证clear()会被执行呢?

这是一个非常关键且常见的问题:在实际开发中,我们经常使用 ThreadLocal 存储线程上下文信息(如用户信息、事务、日志追踪 ID 等),但很多人没有显式调用 clear(),这就可能导致内存泄漏或上下文污染


🧨 问题本质:ThreadLocal 不清理会怎样?

✅ 1. 内存泄漏(Memory Leak)

  • ThreadLocalMap 的 key 是 ThreadLocal 的弱引用(WeakReference),但 value 是强引用。
  • 如果线程执行完任务后,ThreadLocal 被回收(key 为 null),但 value 仍存在 ThreadLocalMap 中。
  • 如果线程是线程池中的线程(长期存活),value 就一直无法被回收,造成内存泄漏。

✅ 2. 上下文污染(Context Pollution)

  • 在线程池中,线程会被复用。
  • 如果不清理,下一个任务可能访问到前一个任务的 ThreadLocal 值,导致数据错乱。
  • 例如:用户 A 的请求结束后没有清理登录信息,下一个用户 B 的请求可能误读为 A 的信息。

✅ 实际开发中如何保证 clear() 被执行?

✅ 方法一:在 try-finally 块中手动清理(最常见)

java

深色版本

void handleRequest(String userId) {UserContext.setCurrentUser(userId);try {// 业务逻辑} finally {UserContext.clear(); // 保证执行}
}

优点:简单直接,适合关键业务逻辑
⚠️ 缺点:容易忘记,不适合大规模使用


✅ 方法二:使用 AOP(如 Spring AOP)自动清理(推荐)

通过 AOP 拦截请求入口,在方法执行前后自动设置和清理 ThreadLocal

示例:Spring AOP + 注解
@Aspect
@Component
public class UserContextAspect {@Around("@annotation(WithUserContext)")public Object withUserContext(ProceedingJoinPoint pjp) throws Throwable {String userId = extractUserId(pjp); // 从参数或请求中获取用户IDUserContext.setCurrentUser(userId);try {return pjp.proceed();} finally {UserContext.clear();}}private String extractUserId(ProceedingJoinPoint pjp) {// 实现从参数中提取用户ID的逻辑return "user123";}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithUserContext {}
@Service
public class SomeService {@WithUserContextpublic void businessMethod() {// 这里可以直接使用 UserContext.getCurrentUser()}
}

优点:统一管理,避免漏掉清理
⚠️ 缺点:需要集成 AOP 框架,配置稍复杂

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

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

相关文章

[故障诊断方向]SNNs:针对小样本轴承故障诊断的孪生神经网络模型

目录 1. ​引言与背景总结​ 2. ​方法框架总结​ 3. ​训练策略总结​ 4. ​实验验证总结​ 核心代码实现&#xff08;PyTorch框架&#xff09; ​1. SNN特征提取器&#xff08;多尺度卷积模块&#xff09; ​结论与未来工作总结​ 1. ​引言与背景总结​ ​问题陈述​…

Java中缓存的使用浅讲

Java中缓存的使用浅讲在Java中&#xff0c;缓存系统的使用对于提升应用性能至关重要。缓存的作用主要是减少访问慢速存储&#xff08;如数据库或文件系统&#xff09;的频率&#xff0c;从而提高应用的响应速度。以下是对Java中缓存系统的全面讲解&#xff0c;包括缓存的类型、…

洛谷 P10264 [GESP202403 八级] 接竹竿 普及+/提高

题目描述 小杨同学想用卡牌玩一种叫做“接竹竿”的游戏。 游戏规则是&#xff1a;每张牌上有一个点数 vvv&#xff0c;将给定的牌依次放入一列牌的末端。若放入之前这列牌中已有与这张牌点数相 同的牌&#xff0c;则小杨同学会将这张牌和点数相同的牌之间的所有牌全部取出队列&…

windows docker-02-docker 最常用的命令汇总

一、镜像管理命令说明常用参数示例docker pull <镜像名>:<标签>拉取镜像docker pull nginx:latestdocker images查看本地镜像docker images -a&#xff08;含中间层镜像&#xff09;docker rmi <镜像ID>删除镜像docker rmi -f $(docker images -q)&#xff0…

前端react项目目录详解

1. 项目根目录文件​​文件/目录作用​​package.json​​定义项目依赖、脚本命令&#xff08;如 start/build&#xff09;、版本信息等​​.env​​基础环境变量配置&#xff08;所有环境共享&#xff09;​​.env.development​​开发环境专用变量&#xff08;如本地API地址&…

前端-CSS (样式引入、选择器)

文章目录大纲前端三大件常用样式颜色px:像素1.CSS三种引入方式1.1 行内样式1.2 页内样式1.3 引入外部样式表文件&#xff08;常见&#xff09;基础选择器1. 标记选择器2. id选择器3. 类选择器 最常用4 * 选择器 使用频率较低复合选择器伪类选择器1.超链接伪类&#xff1a;2.子元…

7月19日 台风“韦帕“强势逼近:一场与时间赛跑的防御战

中央气象台7月19日10时继续发布台风黄色预警,今年第6号台风"韦帕"正以每小时20-25公里的速度向西偏北方向移动,强度逐渐加强。这个来自海洋的"不速之客"中心附近最大风力已达10级(25米/秒),预计将于20日下午至夜间在广东深圳到海南文昌一带沿海登陆,…

学习 Python 爬虫需要哪些基础知识?

学习 Python 爬虫需要掌握一些基础技术和概念。 1. Python 基础语法 这是最根本的前提&#xff0c;需要熟悉&#xff1a; - 变量、数据类型&#xff08;字符串、列表、字典等&#xff09; - 条件判断、循环语句 - 函数、类与对象 - 模块和包的使用&#xff08;如 import 语…

IELTS 阅读C15-Test 2-Passage 2

继续雅思上分实验。这次正确率是10/13&#xff0c;还是挺让我吃惊的&#xff0c;因为我又没有完全读懂&#xff01; 题型1-填空题这道题目很简单&#xff0c;同样地去原文段落里找就好&#xff0c;最后一个空填错了是因为我不知道mitigate就是decrease同义词。 题型2-人物匹配题…

7.18 Java基础 |

以下内容&#xff0c;参考Java 教程 | 菜鸟教程&#xff0c;下边是我边看边记的内容&#xff0c;以便后续复习使用。 多态&#xff1a; 继承&#xff0c;接口就是多态的具体体现方式。生物学上&#xff0c;生物体或物质可以具有许多不同的形式或者阶段。 多态分为运行时多态&…

网络安全知识学习总结 Section 11

一、实验知识总结&#xff08;模拟&#xff09;等价路由配置实验并抓包分析按流分析实验拓扑图&#xff1a;AR1配置&#xff1a;<Huawei>sys [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip address 192.168.1.1 30 [Huawei-GigabitEthernet0/0/0]int g0/0/1 [Huaw…

VBA 运用LISTBOX插件,选择多个选项,并将选中的选项回车录入当前选中的单元格

维护好数据&#xff0c;并新增一个activeX列表框插件Private Sub Worksheet_SelectionChange(ByVal Target As Range)If Target.Count > 1 Then Exit SubIf Target.Row > 2 And Target.Row < 10 And Target.Column 2 Then 选择操作范围With ListBox1.MultiSelect 1 …

ASP .NET Core 8实现实时Web功能

ASP.NET Core SignalR 是一个开放源代码库&#xff0c;可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。以下是 ASP.NET Core SignalR 的一些主要功能&#xff1a;自动处理连接管理同时向所有连接的客户端发送消息。 例如聊天室向特定…

最新版谷歌浏览器 内网安装 pdf无法预览

最新版谷歌浏览器 内网安装 pdf无法预览 谷歌下载地址 谷歌下载地址 不同的浏览器版本&#xff0c;兼容的js标准不一样 js标准也在不断升级&#xff0c;增加新的方法。

NX二次开发常用函数坐标转化UF_MTX4_csys_to_csys和UF_MTX4_vec3_multipl

一、UF_MTX4_csys_to_csys 1.1 函数名称 UF_MTX4_csys_to_csys1.2 函数中各参数解释&#xff1a;函数参数解释&#xff1a; 第1个参数为输入&#xff1a; 输入const double 双精度类型的参数&#xff0c;参数的变量格式为from_origin [ 3 ]&#xff0c;坐标系&#xff…

JAVA中的Collections 类

文章目录前言一、 排序方法 sort() 和 reverseOrder()1. sort(List<T> list)2.sort(List<T> list, Comparator<? super T> c)二、查找方法 max(), min()1.max(Collection<? extends T> coll)2.min(Collection<? extends T> coll)3.max(Collec…

统计学习方法

一、统计学习方法步骤 得到一个有限的训练数据集合确定学习模型的集合-假设空间确定模型选择的准则-策略实现求解最优模型的算法-算法通过学习方法选择最优模型利用学习的最优模型对新数据进行预测或分析 二、统计学习方法分类 三、统计学习的基本分类&#xff08;监督学习&a…

windows docker-01-desktop install windows10 + wls2 启用

windows10 安装 docker 版本信息确认 需要区分 windows 是 amd64 还是 arm64 powershell 中执行&#xff1a; > echo $env:PROCESSOR_ARCHITECTURE AMD64下载 官方 https://www.docker.com/products/docker-desktop/ 下载 windows amd64 下载好了直接安装。 如何验证…

Elasticsearch集群出现脑裂(Split-Brain)如何排查原因和处理?

Elasticsearch集群出现脑裂(Split-Brain)如何排查原因和处理? 1. 脑裂(Split-Brain)背景 定义:脑裂是指 Elasticsearch 集群由于网络分区(network partition)或其他原因分裂成多个独立的子集群,每个子集群认为自己是主集群,导致不同的子集群可能独立处理请求,造成数…

Apache Ignite 的 Pages Writes Throttling(页面写入节流)

&#x1f31f; 一、什么是 Checkpointing&#xff08;检查点机制&#xff09;&#xff1f; 在 Apache Ignite 中&#xff1a; 数据是先保存在内存中&#xff08;RAM&#xff09;&#xff0c;然后异步写入磁盘。当数据被修改时&#xff0c;它首先被更新在内存中的“页”上&#…