🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
ThreadLocal的原理以及实际应用技巧详解
ThreadLocal是什么?
ThreadLocal
是 Java
提供的一种线程局部变量机制。每个线程通过 ThreadLocal
拥有自己独立的变量副本,互不干扰。即多个线程可以使用同一个 ThreadLocal
实例来保存各自的数据,而不必担心线程安全问题。
ThreadLocal能做什么?
没了解过ThreadLocal的小伙伴可能看到上述 ThreadLocal
的介绍还是会一头雾水,那么博主就例举几个样例
❶ 数据隔离
在多线程环境下,每个线程的数据相互隔离,不需要额外同步即可实现线程安全。
public class ThreadLocalDemo {
// 创建 ThreadLocal 对象,并为每个线程提供初始值 0
private static final ThreadLocal<Integer> threadLocalCounter =
ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 定义任务:读取、修改并打印线程局部变量的值
Runnable task = () -> {
// 获取当前线程的 ThreadLocal 变量初始值
int counter = threadLocalCounter.get();
System.out.println(Thread.currentThread().getName() + " 初始计数器: " + counter);
// 模拟计数器的增加操作
counter += 10;
threadLocalCounter.set(counter);
System.out.println(Thread.currentThread().getName() + " 修改后的计数器: " + threadLocalCounter.get());
// 任务结束后,建议调用 remove() 以防内存泄漏
threadLocalCounter.remove();
};
// 启动多个线程,演示各自独立的 ThreadLocal 数据
Thread t1 = new Thread(task, "线程-A");
Thread t2 = new Thread(task, "线程-B");
Thread t3 = new Thread(task, "线程-C");
t1.start();
t2.start();
t3.start();
}
}
输出结果:
从上述代码中,我们就可以发现每一个线程对 counter += 10 都是相互独立互不影响的!
❷ 保存上下文信息
例如,在 Web 应用中,可以使用 ThreadLocal
存储当前用户信息、请求上下文等,方便在方法调用链中传递数据,而不必在方法参数中传递。
假设我们现在有一个需求,要Controller需要接收一个用户ID,获取用户数据、用户的课程,你是否会这样做?
// 第一步:controller 接收参数
getUserInfo?userId=1
// 第二步:调用用户service
userService.getUserById(userId)
// 第三步:调用用户课程service
courseService.getCourseByUserId(userId)
通过上述演示,你会发现每一个service方法都必须要传递一个userId参数,那么采用 ThreadLocal
就可以在方法调用链中传递数据,而不是在方法参数中
// 创建 ThreadLocal 对象
private static final ThreadLocal<Integer> threadLocalUser = new ThreadLocal<>();
// controller 接收参数后调用ThreadLocal.set方法
threadLocalUser.set(userId)
// 在后续service中只需要滴哦用ThreadLocal.get方法即可获取userId值
userId = threadLocalUser.get()
这里暂时先有个了解,后面我们会针对这个进行实际案例的演示
❸ 数据库连接与事务管理
在一些框架中,会使用 ThreadLocal
将数据库连接或者事务绑定到当前线程,确保同一线程内的操作共享同一个连接或事务对象。
// 通过 ThreadLocal,可以确保每个线程都有一个独立的数据库连接,
// 避免了多个线程竞争同一个连接
public class DBConnectionManager {
private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
// 创建并返回数据库连接
return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
});
public static Connection getConnection() {
return connectionHolder.get();
}
public static void closeConnection() throws SQLException {
Connection connection = connectionHolder.get();
if (connection != null) {
connection.close();
}
connectionHolder.remove();
}
}
ThreadLocal 的原理
❶ 核心思想
- 每个线程有自己的存储空间
当你创建一个ThreadLocal
对象时,并不会在ThreadLocal
本身保存数据,而是每个线程内部都有一个ThreadLocalMap
(作为线程的成员变量)来存储数据。 - 数据隔离
当线程调用threadLocal.get()
或set()
时,实际上是对当前线程中的ThreadLocalMap
进行操作。这样不同线程之间的数据不会相互影响。
❷ 底层实现
-
ThreadLocalMap
每个Thread
对象内部都有一个ThreadLocalMap
实例,结构类似于一个哈希表。
ThreadLocalMap
内部存储的是一个个Entry
对象,每个Entry
存储了一对[ key,value ] , 即:[ThreadLocal 键 (以弱引用形式存在) , 实际值] -
弱引用
ThreadLocal
在ThreadLocalMap
中的键是弱引用,这样设计的目的是防止内存泄露:如果ThreadLocal
对象不再被外部引用,GC 可以回收该对象,但其对应的value
仍可能存在于ThreadLocalMap
中,这就需要我们在使用完毕后调用remove()
方法清理数据。 -
哈希算法与线性探测
ThreadLocalMap
使用数组来存储 Entry,通过散列算法确定索引,冲突时采用线性探测来寻找下一个空位。
说明:
- ThreadLocal 本身不保存数据,数据存储在各个线程内部的 ThreadLocalMap 中。
- Entry 中的 key 是 ThreadLocal 对象的弱引用,以便防止内存泄露;value 是当前线程对应的实际数据。
典型的场景案例
接下来博主介绍一种典型的场景,也为了更详细说明 ThreadLocal
保存上下文信息
场景说明:
在前后端分离的 Spring Boot
项目中,前端通过请求头或 Token
将租户 ID、用户 ID 等信息传递给后端。后端在请求入口(如过滤器或拦截器)中解析这些信息,并存入 ThreadLocal 中,后续的业务代码就可以直接从 ThreadLocal
中获取,不必通过方法参数逐层传递。
在多租户和用户身份认证场景中,为了在业务逻辑中方便地获取当前请求的租户信息和用户信息,通常采用以下方式:
-
前端传递信息
前端请求时会在HTTP
请求头中附带Token
或自定义header
(例如 X-Tenant-ID、Authorization),其中包含了租户 ID 和用户 ID。 -
后端解析并存储
在请求入口处(例如基于 Spring 的过滤器或拦截器),解析Token
或Header
后,将租户 ID 和用户 ID 保存到一个基于ThreadLocal
的工具类中,确保当前线程内的所有代码都能方便地获取到这些数据。 -
业务使用
在Controller
或Service
中,通过调用该工具类的方法,即可获取当前线程对应的租户和用户信息,进而进行数据隔离、权限判断等处理。 -
清理资源
请求结束后,务必清理ThreadLocal
中的数据,以防止内存泄漏,尤其在使用线程池的情况下尤为重要。
代码实现
❶ 定义上下文对象
定义一个简单的上下文 POJO,用于封装租户和用户信息:
public class UserContext {
//租户ID
private String tenantId;
//用户ID
private String userId;
// Getter 和 Setter 方法
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
❷ 创建 ThreadLocal 工具类
利用 ThreadLocal
存储当前线程的 UserContext:
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContextThreadLocal = new ThreadLocal<>();
public static void setContext(UserContext context) {
userContextThreadLocal.set(context);
}
public static UserContext getContext() {
return userContextThreadLocal.get();
}
public static void clear() {
userContextThreadLocal.remove();
}
}
❸ 编写过滤器解析请求并设置上下文
通过 Spring 的过滤器,在请求到达 Controller 前解析请求头信息,并将其设置到 ThreadLocal
中:
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class AuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 从请求头中获取租户ID和用户ID(或从 Token 中解析)
String tenantId = request.getHeader("X-Tenant-ID");
String userId = request.getHeader("X-User-ID");
// 这里也可以做 Token 解析,例如使用 JWT 库获取其中的信息
// TODO
// 封装到 UserContext 对象中
UserContext context = new UserContext();
context.setTenantId(tenantId);
context.setUserId(userId);
// 设置到 ThreadLocal 中
UserContextHolder.setContext(context);
// 继续调用后续的过滤器/处理逻辑
filterChain.doFilter(request, response);
} finally {
// 请求结束后清理 ThreadLocal 数据,防止内存泄露
UserContextHolder.clear();
}
}
}
上述代码中,仅简单演示自定义Header X-User-ID 获取用户ID,我们也可以做 Token 解析,例如使用 JWT 库获取其中的信息,然后给UserContext设值
❹ 业务代码中使用上下文信息
在 Controller 或 Service 中,通过工具类获取当前线程对应的租户 ID 和用户 ID:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/info")
public String getInfo() {
UserContext context = UserContextHolder.getContext();
if (context != null) {
return "租户ID: " + context.getTenantId() + ",用户ID: " + context.getUserId();
}
return "无用户信息";
}
}
通过这种方式,无论后续调用链有多少层,都可以直接通过 UserContextHolder.getContext() 获取到当前请求对应的租户和用户信息,而不必在每个方法中都传递参数。
最终实现流程
该图展示了:请求从前端发出,经过过滤器解析后,将用户信息存入 ThreadLocal,随后在业务逻辑中获取该信息,最终完成响应。
总结
ThreadLocal
提供了一种简单的线程数据隔离方式,使每个线程可以拥有独立的数据副本。- 底层实现依赖于每个线程内部的
ThreadLocalMap
,通过弱引用机制以及数组存储来实现数据隔离和查找。 - 在实际应用中,
ThreadLocal
常用于保存线程上下文、数据库连接、事务管理等场景
相信看到这里,通过博主的讲解以及代码示例,相信小伙伴们已经掌握了ThreadLocal的原理及使用,如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!