ThreadLocal的原理以及实际应用技巧详解 - 如何在身份认证场景Token中传递获取用户信息

在这里插入图片描述

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

ThreadLocal是什么?

ThreadLocalJava 提供的一种线程局部变量机制。每个线程通过 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 键 (以弱引用形式存在) , 实际值]

  • 弱引用
    ThreadLocalThreadLocalMap 中的键是弱引用,这样设计的目的是防止内存泄露:如果 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 的过滤器或拦截器),解析 TokenHeader 后,将租户 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的原理及使用,如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micro麦可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值