多线程和 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变量
java">private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
2. 操作数据
java">// 设置值(仅当前线程可见)
threadLocal.set("Hello");// 获取值
String value = threadLocal.get(); // 输出 "Hello"// 删除值(防止内存泄漏)
threadLocal.remove();
注意事项:使用完ThreadLocal后必须调用remove()清除数据,防止内存泄漏。
2.4 ThreadLocal的典型使用场景
1. 线程级上下文管理:
在Web应用中存储用户登录信息(如用户ID),避免在方法参数中层层传递。
java">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。
java">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 为每个线程创建独立实例。
java">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上下文工具类:
java">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
java">public class AuthInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 1. 从请求头中获取 Token(示例)String token = request.getHeader("Authorization");// 2. 解析 Token 获取用户信息(实际需调用认证服务或查询数据库)LoginUser user = parseToken(token);// 3. 存入 ThreadLocalUserContext.setUser(user);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 请求结束后清理 ThreadLocalUserContext.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配置)
java">@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns("/login"); // 排除登录接口}
}
在业务层进行权限验证
java">@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
,但要注意线程池中复用线程时可能失效。