Java 中的线程本地存储(ThreadLocal)机制介绍

Java 中的 ThreadLocal 是一个用于实现线程本地存储(Thread Local Storage, TLS)的机制。它可以为每个线程提供独立的变量副本,使得一个线程中的变量不受其他线程中的变量的影响。ThreadLocal 通常用于在多线程环境下避免线程之间共享数据,从而实现线程安全。

一、基本用法

ThreadLocal 类提供了一种机制,允许线程在本地存储一些变量,并在相同线程中获取这些变量。每个线程都有一个独立的 ThreadLocal 实例,可以保存自己的值,其他线程无法访问。

1. 创建和使用 ThreadLocal

// 创建一个 ThreadLocal 实例
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

// 在当前线程中设置值
threadLocal.set(100);

// 在当前线程中获取值
Integer value = threadLocal.get();

// 移除当前线程的值
threadLocal.remove();

每个线程在调用 set() 方法时,会将值存储在当前线程的 ThreadLocal 实例中,并且该值对其他线程不可见。同样地,get() 方法会返回当前线程存储的值。

2. 使用 initialValue() 初始化值

ThreadLocal 还可以通过重写 initialValue() 方法来设置初始值:

ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

// 当前线程第一次调用 get() 时返回初始值 0
Integer initialValue = threadLocal.get();

这种方法可以避免每次使用前都调用 set() 方法来设置初始值。

3. 使用 ThreadLocal.withInitial() 简化初始化

Java 8 引入了 ThreadLocal.withInitial() 方法,允许使用 Lambda 表达式更简洁地设置初始值:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
Integer initialValue = threadLocal.get();

二、ThreadLocal 的工作原理

1 . ThreadLocal 的工作机制

  • ThreadLocal 为每个线程独立存储数据,即每个线程都有自己独立的一份 ThreadLocal 数据副本。因此,线程之间不会共享 ThreadLocal 中的数据。
  • 当一个线程处理某个 HTTP 请求时,它会将数据存储到 ThreadLocal 中,其他线程无法访问或修改该数据。也就是说,ThreadLocal 保证了每个线程存储的数据是独立的,因此不会发生数据重复或相互覆盖的问题。

2. 并发请求中的表现

  • 在并发请求量大的情况下,由于每个请求都会分配到不同的线程处理,因此每个线程都维护自己的 ThreadLocal 变量,不会相互影响。

  • 但需要注意的是,线程池 的存在会引入一些复杂性。大多数应用服务器都会使用线程池来处理请求,即相同的线程可能会在不同时间处理不同的请求。

    • 例如,线程 A 处理了请求 1,然后它的 ThreadLocal 中保存了用户信息。处理完请求 1 后,如果不调用 clear(),线程 A 上存储的用户信息仍然存在。
    • 线程 A 可能会再次被分配去处理请求 2,此时如果没有正确清理 ThreadLocal,线程 A 上存储的用户信息可能会导致意外的行为,产生数据泄漏或错误。

    因此,在请求处理完毕后一定要确保调用 clear() 方法,以清理 ThreadLocal,防止线程池重用线程时出现数据污染的问题

三、注意事项

  1. 内存泄漏问题: 由于 ThreadLocalMap 的键是一个弱引用(WeakReference),而值是强引用,可能会导致内存泄漏问题。特别是在使用线程池时,线程不会被销毁,因此 ThreadLocal 的数据可能会长期存在内存中。为了避免这种问题,建议在线程使用完 ThreadLocal 后显式调用 remove() 方法清理数据。

  2. 线程池中的使用: 在使用线程池时,由于线程会被重用,必须特别小心 ThreadLocal 的使用。如果不在适当的时机清理 ThreadLocal,下一个任务可能会意外地获取到上一个任务的值。

  3. 性能问题:对于频繁创建和销毁线程的场景,ThreadLocal 的创建和销毁开销可能较大,因此更适合于线程池等长生命周期的线程管理场景

四、示例:存储与请求相关的数据,如当前登录用户的信息

1. 编写ThreadLocal工具类

理论上我们可以在Controller方法中,使用@RequestHeader获取JWT,然后在进行解析,如下

@Operation(summary = "获取登陆用户个人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info(@RequestHeader("Authorization") String token) {
    Claims claims = JwtUtil.parseToken(token);
    Long userId = claims.get("userId", Long.class);
    SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);
    return Result.ok(userInfo);
}

上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。

ThreadLocal概述

ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,使每个线程都可以操作自己的变量,而不会互相干扰,其用法如下图所示。

common模块中创建com.atguigu.lease.common.login.LoginUserHolder工具类

public class LoginUserHolder {
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    public static void setLoginUser(LoginUser loginUser) {
        threadLocal.set(loginUser);
    }

    public static LoginUser getLoginUser() {
        return threadLocal.get();
    }

    public static void clear() {
        threadLocal.remove();
    }
}

同时在common模块中创建com.atguigu.lease.common.login.LoginUser

@Data
@AllArgsConstructor
public class LoginUser {

    private Long userId;
    private String username;
}

2. 修改AuthenticationInterceptor拦截器

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("Authorization");

        Claims claims = JwtUtil.parseToken(token);
        Long userId = claims.get("userId", Long.class);
        String username = claims.get("username", String.class);
        LoginUserHolder.setLoginUser(new LoginUser(userId, username));

        return true;

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginUserHolder.clear();
    }
}

3. 编写Controller层逻辑

LoginController中增加如下内容

@Operation(summary = "获取登陆用户个人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info() {
    SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
    return Result.ok(userInfo);
}

4. 编写Service层逻辑

在`LoginService`中增加如下内容

```java
@Override
public SystemUserInfoVo getLoginUserInfo(Long userId) {
    SystemUser systemUser = systemUserMapper.selectById(userId);
    SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();
    systemUserInfoVo.setName(systemUser.getName());
    systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());
    return systemUserInfoVo;
}
```

五、总结

ThreadLocal 提供了一种简单而有效的方式来为每个线程存储独立的数据,避免了线程间共享数据所导致的线程安全问题。然而,需要谨慎使用,尤其是在使用线程池或长生命周期线程时,以避免潜在的内存泄漏问题。

posted @   槑孒  阅读(282)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
历史上的今天:
2023-08-15 [Vue warn]: Runtime directive used on component with non-element root node. The directives will not function as intended.
2023-08-15 网页图标(Favicon)
2023-08-15 屏蔽 Use :deep() instead.警告
2023-08-15 nodejs的版本管理工具NVM
2022-08-15 QGIS实现PostGIS数据库查询并返回新图层
2022-08-15 docker 设置国内镜像源
2022-08-15 ArcGIS Pro连接地理数据库(PostGIS)
点击右上角即可分享
微信分享提示