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();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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

2. 使用 initialValue() 初始化值

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

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

// 当前线程第一次调用 get() 时返回初始值 0
Integer initialValue = threadLocal.get();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

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

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

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

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

二、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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

ThreadLocal概述

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

Java 中的线程本地存储(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();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

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

@Data
@AllArgsConstructor
public class LoginUser {

    private Long userId;
    private String username;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
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();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
3. 编写Controller层逻辑

LoginController中增加如下内容

@Operation(summary = "获取登陆用户个人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info() {
    SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
    return Result.ok(userInfo);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
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;
}
```
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

五、总结

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