多线程和 ThreadLocal 是 Java 并发编程中的两个重要概念,它们在处理线程安全和资源隔离时扮演关键角色。
1. 多线程基础
1.1 什么是多线程?
线程:是操作系统能够调度的最小执行单元,一个进程可以包含多个线程。
多线程:指程序中存在多个线程并发执行,以提高 CPU 利用率或实现异步任务处理。
并发问题:当多个线程共享同一资源时,可能引发竞态条件(Race Condition)、数据不一致等问题。
1.2 线程的生命周期
线程有以下状态:
New:线程被创建但未启动。
Runnable:线程正在执行或等待 CPU 时间片。
Blocked/Waiting:线程因等待锁、I/O 或条件变量而暂停。
Terminated:线程执行完毕或异常终止。
1.3 线程同步机制
为了解决并发问题,常用的同步工具有:
synchronized:通过锁机制保证代码块或方法的原子性。
Lock:如
ReentrantLock
,提供更灵活的锁控制。volatile:保证变量的可见性(但不保证原子性)。
Atomic 类:如
AtomicInteger
,通过 CAS(Compare-and-Swap)实现无锁线程安全。
2. ThreadLocal的引入
在多线程环境中,共享变量的线程安全问题通常需要通过同步机制解决。但某些场景下,我们希望每个线程拥有自己的变量副本,避免共享,从而彻底消除同步需求,例如:
- 数据库连接管理(每个线程独立连接)。
- 用户会话信息(每个请求对应一个线程)。
2.1 什么是ThreadLocal?
ThreadLocal 是 Java 中用于实现线程封闭的工具类,它能为每个线程创建一个独立的变量副本,不同线程之间互不干扰。简单来说,ThreadLocal 让每个线程都能拥有自己的“私有”数据。
假设有一个储物柜(ThreadLocal
),每个线程(人)都可以往自己的储物柜里存东西,且只能访问自己的储物柜,不会拿错别人的东西。
2.2 ThreadLocal的核心机制
1. 线程隔离
- 每个线程内部有一个
ThreadLocalMap
(类似Map结构),用来存储该线程的所有ThreadLocal
变量。 - 不同线程的
ThreadLocalMap
彼此独立,互不可见。
2. 存储结构
- 每个
ThreadLocal
对象通过threadLocalHashCode
唯一标识自己。 ThreadLocalMap
的 key 是ThreadLocal
对象,value 是线程存储的值。
2.3 ThreadLocal的使用方法
1. 创建ThreadLocal变量
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
2. 操作数据
// 设置值(仅当前线程可见)
threadLocal.set("Hello");
// 获取值
String value = threadLocal.get(); // 输出 "Hello"
// 删除值(防止内存泄漏)
threadLocal.remove();
注意事项:使用完ThreadLocal后必须调用remove()清除数据,防止内存泄漏。
2.4 ThreadLocal的典型使用场景
1. 线程级上下文管理:
在Web应用中存储用户登录信息(如用户ID),避免在方法参数中层层传递。
public class UserContext {
private static ThreadLocal<Long> userHolder = new ThreadLocal<>();
public static void setUserId(Long userId) {
userHolder.set(userId);
}
public static Long getUserId() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
2. 数据库连接管理:
在事务中,保证一个线程的所有数据库操作使用同一个 Connection。
public class ConnectionHolder {
private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
return dataSource.getConnection(); // 初始化连接
});
public static Connection getConnection() {
return connectionHolder.get();
}
public static void close() {
connectionHolder.get().close();
connectionHolder.remove();
}
}
3. 日期格式化:
SimpleDateFormat 是线程不安全的,可用 ThreadLocal 为每个线程创建独立实例。
private static ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
3. ThreadLocal实现权限验证
使用 ThreadLocal 实现权限验证是一种常见的实践,尤其在 Web 应用中,比如用户登录后,后续的请求需要携带token,并在服务端验证权限。通过将用户身份信息(如用户ID、角色、权限列表)存储在 ThreadLocal
中,可以避免在方法参数中层层传递用户上下文,同时保证线程安全。可以在Spring Security或者自定义的拦截器中,获取用户信息后存入ThreadLocal,后续业务方法可以直接使用。
3.1 实现思路
- 拦截请求:在请求进入时(如通过过滤器或拦截器),解析Token或Cookie获取用户身份信息。
- 存储用户信息:将用户信息存入ThreadLocal,后续业务代码可直接从ThreadLocal中获取。
- 权限验证:在需要权限控制的地方(如Service层或自定义注解),从ThreadLocal取出用户信息,验证权限。
- 清理数据:请求结束时(如拦截器后置处理),清理ThreadLocal,防止内存泄漏。
3.2 代码实现
定义ThreadLocal上下文工具类:
public class UserContext {
// 存储用户信息的 ThreadLocal(使用静态内部类初始化,避免内存泄漏)
private static final ThreadLocal<LoginUser> userHolder = new ThreadLocal<>();
// 设置当前用户
public static void setUser(LoginUser user) {
userHolder.set(user);
}
// 获取当前用户
public static LoginUser getUser() {
return userHolder.get();
}
// 清理用户信息(必须调用!)
public static void clear() {
userHolder.remove();
}
// 用户信息封装类(示例)
public static class LoginUser {
private Long userId;
private String username;
private List<String> roles; // 角色列表(如 "admin", "user")
private List<String> permissions; // 权限列表(如 "order:create", "user:delete")
// 省略构造方法、getter/setter
}
}
拦截器中解析用户信息并存入ThreadLocal
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从请求头中获取 Token(示例)
String token = request.getHeader("Authorization");
// 2. 解析 Token 获取用户信息(实际需调用认证服务或查询数据库)
LoginUser user = parseToken(token);
// 3. 存入 ThreadLocal
UserContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后清理 ThreadLocal
UserContext.clear();
}
// 模拟 Token 解析逻辑
private LoginUser parseToken(String token) {
// 实际需验证 Token 合法性,并查询用户权限(可结合 JWT 或 Redis)
LoginUser user = new LoginUser();
user.setUserId(1001L);
user.setUsername("admin");
user.setRoles(Arrays.asList("admin"));
user.setPermissions(Arrays.asList("user:delete", "order:*"));
return user;
}
}
注册拦截器(Spring Boot配置)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除登录接口
}
}
在业务层进行权限验证
@Service
public class OrderService {
public void deleteOrder(Long orderId) {
// 从 ThreadLocal 获取当前用户
LoginUser user = UserContext.getUser();
// 1. 验证是否登录
if (user == null) {
throw new RuntimeException("未登录!");
}
// 2. 验证权限(示例:需要 "order:delete" 权限)
if (!user.getPermissions().contains("order:delete")) {
throw new RuntimeException("权限不足!");
}
// 3. 执行业务逻辑
// ...
}
}
3.3 注意事项
1. 内存泄漏:
必须确保在请求结束时调用 UserContext.clear()(通常在拦截器的 afterCompletion 中清理)。
如果使用线程池(如异步任务),线程会被复用,必须显式清理 ThreadLocal。
2. 分布式系统:
ThreadLocal 只适用于单机环境。若服务是分布式的,用户信息应存储在 Redis 等共享存储中,通过 Token 解析获取。
3. 安全性:
Token 解析逻辑需要严格防止伪造(如使用 JWT 签名或查询 Redis 校验有效性)。
4. 性能:
频繁调用 ThreadLocal.get() 对性能影响极小,但权限列表不宜过大(建议控制在百级以内)。
3.4 适用场景
- 单体架构需要Web应用(如Spring Boot后台管理系统)。
- 需要减少方法参数传递的上下文信息(如用户身份)。
- 配合AOP实现声明式权限控制。
4. 常见问题
4.1 ThreadLocal和synchronized的区别
ThreadLocal
通过空间换时间,每个线程独立操作数据,无需竞争锁。synchronized
通过时间换空间,让线程排队访问共享数据。
4.2 子线程如何继承父线程的ThreadLocal数据
- 使用
InheritableThreadLocal
,但要注意线程池中复用线程时可能失效。