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
,防止线程池重用线程时出现数据污染的问题 - 例如,线程
三、注意事项
-
内存泄漏问题: 由于
ThreadLocalMap
的键是一个弱引用(WeakReference
),而值是强引用,可能会导致内存泄漏问题。特别是在使用线程池时,线程不会被销毁,因此ThreadLocal
的数据可能会长期存在内存中。为了避免这种问题,建议在线程使用完ThreadLocal
后显式调用remove()
方法清理数据。 -
线程池中的使用: 在使用线程池时,由于线程会被重用,必须特别小心
ThreadLocal
的使用。如果不在适当的时机清理ThreadLocal
,下一个任务可能会意外地获取到上一个任务的值。 -
性能问题:对于频繁创建和销毁线程的场景,
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
提供了一种简单而有效的方式来为每个线程存储独立的数据,避免了线程间共享数据所导致的线程安全问题。然而,需要谨慎使用,尤其是在使用线程池或长生命周期线程时,以避免潜在的内存泄漏问题。