芒果浏览器:不止是“尝鲜”,更是数字生活的全新“滋味”

核心内容摘要

2015小明加密免费平台登录入口:开启你的数字自由新篇章!
辶喿辶喿姐弟:探索网络用语中的独特魅力

绝色之境与灵魂共振:日本91大片震撼上映,重塑你对东方美学的极致认知

在 Java Web 项目开发中我们经常会遇到一个问题一次 HTTP 请求的处理流程会跨越 Controller、Service、Mapper 等多个层级若需要在这些层级间传递通用数据比如当前登录用户 ID层层显式传参不仅代码繁琐还会让方法签名变得臃肿。

而 JDK 提供的 ThreadLocal 工具正是解决这一问题的最优解之一。

在实际项目中我们会基于 ThreadLocal 封装出 BaseContext 这样的核心工具类它就像线程的 “隐形口袋”能在同一个请求的处理线程中安全、优雅地传递数据实现线程隔离的数据共享。

今天就从实战角度聊聊 ThreadLocal 的核心原理、使用方式以及项目中的最佳实践。

ThreadLocal 是什么—— 线程的 “专属储物柜”要理解 ThreadLocal首先要抛开它的字面意思很多人会误以为是 “本地线程”它的核心定义是为每个使用该变量的线程提供独立的变量副本每个线程都可以独立地修改自己的副本而不会影响其他线程对应的副本。

我们可以把 ThreadLocal 想象成每个线程的 “专属储物柜”线程 A 往自己的储物柜里放了数据线程 B 完全看不到也无法修改线程之间的数据实现了彻底的隔离。

这种特性让 ThreadLocal 天生适合解决多线程环境下的数据隔离问题尤其是 Web 项目中的请求级数据传递。

在项目的 BaseContext 类中核心就是声明了一个 ThreadLocal 对象用于存储当前登录用户的 ID代码非常简洁/** * 基于ThreadLocal封装的上下文工具类用于传递当前登录用户ID */ public class BaseContext { // 定义ThreadLocal变量泛型为Long存储用户ID public static ThreadLocalLong threadLocal new ThreadLocal(); /** * 存入用户ID * param id 当前登录用户ID */ public static void setCurrentId(Long id) { threadLocal.set(id); } /** * 获取用户ID * return 当前登录用户ID */ public static Long getCurrentId() { return threadLocal.get(); } /** * 移除用户ID */ public static void removeCurrentId() { threadLocal.remove(); } }这几行代码就是 BaseContext 的全部核心它基于 ThreadLocal 封装了存、取、删三个方法专门用于用户 ID 的传递后续所有业务层代码都可以通过这个工具类快速获取当前登录用户信息无需任何参数传递。

为什么 Web 项目需要 ThreadLocal—— 从 Tomcat 线程池说起Web 项目的底层服务器如 Tomcat都是基于线程池工作的这是理解 ThreadLocal 在 Web 项目中应用的关键每当一个 HTTP 请求发送到后端Tomcat 会从线程池中分配一个独立的线程来处理这次请求从 Controller 接收请求到 Service 处理业务再到 Mapper 操作数据库整个流程都在这个线程中执行不同请求对应不同的线程线程之间相互独立不会互相干扰若没有 ThreadLocal要在 Controller→Service→Mapper 之间传递用户 ID只能通过方法参数层层传递比如 Controller 接收用户 ID 后调用 Service 方法时传入Service 调用 Mapper 时再传入代码会变得极其繁琐。

而 ThreadLocal 的出现正好完美适配了这种场景同一个请求的所有处理逻辑都在同一个线程中执行只要在该线程中存入数据整个处理流程都能随时取出且数据仅对当前线程可见。

简单来说ThreadLocal 让我们在同一个线程内实现数据共享在不同线程间实现数据隔离既解决了多层级数据传递的问题又保证了多线程环境下的数据安全。

ThreadLocal 的项目实战流程 ——BaseContext 的完整工作链路基于 BaseContext 类用户 ID 的传递在项目中是一个全自动、无感知的过程整个流程分为存入数据、取出数据、清理数据三个步骤核心依托于 SpringMVC 的拦截器Interceptor实现完美融入请求的处理生命周期。

步骤 1存入数据 —— 拦截器层解析 Token初始化用户 ID当请求到达后端时不会直接进入 Controller而是先经过拦截器比如项目中的 JwtTokenAdminInterceptor拦截器的核心作用是校验请求的合法性比如解析请求头中的 JWT 令牌验证令牌是否有效并从令牌中提取当前登录用户的 ID。

在令牌校验通过后就可以通过 BaseContext 将用户 ID 存入当前线程的 ThreadLocal 中代码示例如下/** * 管理员端令牌拦截器校验用户登录状态 */ public class JwtTokenAdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //

从请求头中获取令牌 String token request.getHeader(token); //

解析令牌校验有效性省略令牌校验逻辑 Long userId JwtUtil.parseToken(token); // 从令牌中提取用户ID //

将用户ID存入ThreadLocal BaseContext.setCurrentId(userId); //

放行请求进入Controller return true; } }拦截器的 preHandle 方法会在 Controller 方法执行之前执行此时我们将用户 ID 存入 ThreadLocal后续整个请求的处理流程只要在同一个线程中都能随时获取到这个用户 ID。

步骤 2取出数据 —— 业务层直接调用无需参数传递当请求通过拦截器进入 Service 层后若业务逻辑需要用到当前登录用户 ID比如新增数据时记录创建人、修改数据时记录修改人直接调用 BaseContext.getCurrentId () 即可无需 Controller 层传递任何参数。

以项目中新增员工的业务为例Service 层代码如下Service public class EmployeeServiceImpl implements EmployeeService { Autowired private EmployeeMapper employeeMapper; Override public void save(EmployeeDTO employeeDTO) { Employee employee new Employee(); // 对象属性拷贝省略 BeanUtils.copyProperties(employeeDTO, employee); // 直接从BaseContext获取当前登录用户ID无需参数传递 Long currentUserId BaseContext.getCurrentId(); // 设置创建人、修改人 employee.setCreateUser(currentUserId); employee.setUpdateUser(currentUserId); // 设置创建时间、修改时间 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); // 插入数据库 employeeMapper.insert(employee); } }可以看到Service 层代码完全不需要关心用户 ID 是从哪里来的也不需要 Controller 层传参一行代码就能获取到当前登录用户的 ID代码简洁且优雅后续无论业务逻辑如何修改都无需调整参数传递方式。

即使是 Mapper 层若需要直接使用用户 ID也可以通过 BaseContext.getCurrentId () 获取实现了从 Controller 到 Mapper 的全链路无参数传递。

步骤 3清理数据 —— 请求结束后释放资源避免内存泄漏这是一个极易被忽略但极其重要的步骤Tomcat 的线程池会回收复用线程如果请求处理完毕后不清理 ThreadLocal 中的数据这些数据会一直存留在线程中当线程被复用于处理其他请求时可能会导致数据混淆更严重的是会造成内存泄漏因为 ThreadLocal 的底层实现会存在弱引用问题未清理的数据会让对象无法被 GC 回收。

因此我们需要在请求处理完毕后手动清理 ThreadLocal 中的数据这个操作同样在拦截器中实现依托于拦截器的 afterCompletion 方法 —— 该方法会在请求处理完成后包括异常情况执行代码示例如下Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清理ThreadLocal中的用户ID释放资源 BaseContext.removeCurrentId(); }这一行代码就能保证每次请求处理完毕后当前线程的 ThreadLocal 都会被清空线程被回收复用后不会携带任何上一次请求的数据从根本上避免了数据混淆和内存泄漏问题。

ThreadLocal 的核心

注意事项 —— 这些坑一定要避开ThreadLocal 虽然好用但如果使用不当很容易引发问题结合项目实战

总结了几个核心

注意事项也是开发中最容易踩的坑

必须手动清理数据避免内存泄漏和数据混淆这是最重要的一点再次强调使用 ThreadLocal 存储数据后一定要在合适的时机调用 remove () 方法清理数据。

尤其是在使用线程池的场景下如 Tomcat 线程池线程会被复用若不清理数据会一直存在于线程中导致后续请求获取到错误的数据。

最佳实践就是像项目中的 BaseContext 一样在请求结束后通过拦截器的 afterCompletion 方法统一清理无论请求处理成功还是失败都会执行清理操作。

ThreadLocal 的数据仅对当前线程可见不要试图在多线程场景下通过 ThreadLocal 共享数据比如在主线程中存入数据在子线程中取出这是无法实现的因为子线程是一个独立的线程拥有自己的 ThreadLocal 副本无法访问主线程的数据。

如果需要在父子线程间传递数据可以使用 JDK 提供的InheritableThreadLocal它是 ThreadLocal 的子类能实现父子线程间的数据继承但同样需要注意清理数据。

避免使用 static 修饰 ThreadLocal视场景而定在项目的 BaseContext 中我们将 ThreadLocal 声明为 static这是因为 BaseContext 是一个工具类被所有线程共享static 修饰的 ThreadLocal 能保证所有线程使用的是同一个 ThreadLocal 对象而每个线程存储的是自己的副本这是符合业务场景的。

但如果是非工具类场景避免随意使用 static 修饰 ThreadLocal防止不必要的线程间影响。

不要在多线程异步处理中使用 ThreadLocal在项目中如果使用了 Async 等注解实现异步处理异步方法会在新的线程中执行此时无法获取到主线程 ThreadLocal 中的数据因为异步线程是独立的和主线程无关联。

若异步处理需要传递数据建议通过方法参数显式传递而不是依赖 ThreadLocal。

五、

总结 ——ThreadLocal 的

核心价值回到项目中的 BaseContext 类它仅仅是对 ThreadLocal 做了一层简单的封装却解决了 Web 项目中多层级数据传递的核心问题这正是 ThreadLocal 的价值所在简化代码避免了多层级方法的显式参数传递让代码更简洁、更优雅数据隔离保证多线程环境下的数据安全不同线程之间的数据互不干扰生命周期适配完美适配 Web 项目的请求生命周期同一个请求的所有处理逻辑共享同一套数据低侵入性基于工具类封装后业务层代码无需关心数据的传递过程只需直接调用即可对业务代码无侵入。

ThreadLocal 并不是什么高深的技术但其设计思想非常巧妙 ——以空间换时间通过为每个线程创建独立的副本实现线程隔离的数据共享。

在 Web 项目中除了传递用户 IDThreadLocal 还可以用于传递请求 ID、请求头信息、日志上下文等通用数据是开发中不可或缺的工具。

而项目中的 BaseContext 类正是 ThreadLocal 在实际开发中的最佳实践之一它让我们看到优秀的代码往往不是复杂的而是用最简单的技术解决最核心的问题。

香蕉文化-香蕉文化应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123