Java项目开发中ThreadLocal的6大用法总结
作者:程序员大华
前言
你的系统需要记录每个用户的操作日志,包括用户ID、操作时间、操作内容等。在单线程环境下,这很简单,一个全局变量就够了。
但到了Web应用中,一个请求一个线程,多个用户同时操作,怎么保证A用户的操作不会被记录成B用户呢?
你可能会想到在每个方法里都传递用户信息,但这样太麻烦了。
这就是 ThreadLocal 要解决的核心问题:在多线程环境下,如何在不传递参数的情况下,让同一个线程内的多个方法共享数据,同时又不会影响其他线程?
ThreadLocal是什么
简单理解,ThreadLocal 是一个让每个线程拥有自己独立变量副本的容器。
对同一个 ThreadLocal 实例的 get()/set(),不同线程看到的是不同的数据,互不干扰。
看个最简单的例子:
public class UserContext {
// 创建一个ThreadLocal变量,用来存储用户ID
private static final ThreadLocal<String> userIdHolder = new ThreadLocal<>();
// 设置用户ID
public static void setUserId(String userId) {
userIdHolder.set(userId);
}
// 获取用户ID
public static String getUserId() {
return userIdHolder.get();
}
// 清理用户ID
public static void clear() {
userIdHolder.remove();
}
}
使用起来是这样的:
try {
UserContext.setUserId(userId);
// 业务处理...
} finally {
// 确保处理完请求后,无论如何都会清理
UserContext.clear();
}
// 在后续的业务逻辑中,任何地方都可以获取到这个用户ID
String userId = UserContext.getUserId();
是不是很方便?不用在每个方法参数里传递用户ID,任何地方都可以获取到当前线程的用户信息。
ThreadLocal是怎么工作的
很多人以为 ThreadLocal 只是简单的把变量复制到每个线程,其实不是这样。
它的原理是这样的:
- 每个线程(Thread对象)内部都有一个特殊的Map,叫做
ThreadLocalMap。 - 当你调用
threadLocal.set(value)时,实际上是把 value 放到了当前线程的ThreadLocalMap中,key 是这个 ThreadLocal 对象。 - 当你调用
threadLocal.get()时,实际上是去当前线程的 ThreadLocalMap 中,找这个 ThreadLocal 对象对应的 value。
可以想象成每个线程都有一个小本本(ThreadLocalMap),这个小本本上记录着各种 ThreadLocal 变量的值。
当你调用某个 ThreadLocal 的 get 方法,就是在小本本上查找对应的内容。
ThreadLocal 使用场景和示例
1. 用户会话管理(最常用)
在Web开发中,这是最经典的使用场景。用户登录后,将用户信息存入 ThreadLocal,在后续的任何地方都能获取。
// 用户上下文工具类
public class UserContext {
private static final ThreadLocal<UserInfo> currentUser = new ThreadLocal<>();
public static void setUser(UserInfo user) {
currentUser.set(user);
}
public static UserInfo getUser() {
return currentUser.get();
}
public static Long getUserId() {
UserInfo user = currentUser.get();
return user != null ? user.getId() : null;
}
public static String getUsername() {
UserInfo user = currentUser.get();
return user != null ? user.getUsername() : null;
}
public static void clear() {
currentUser.remove();
}
}
// 在拦截器中设置用户信息
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从token或session中获取用户信息
String token = request.getHeader("Authorization");
UserInfo user = authService.getUserByToken(token);
if (user != null) {
UserContext.setUser(user);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后清理ThreadLocal,防止内存泄漏
UserContext.clear();
}
}
// 在业务代码中直接使用
@Service
public class OrderService {
public void createOrder(Order order) {
// 直接获取当前用户ID,不需要从参数传递
Long userId = UserContext.getUserId();
order.setUserId(userId);
order.setCreateBy(UserContext.getUsername());
// 其他业务逻辑...
orderDao.save(order);
// 记录操作日志
logService.log(userId, "创建订单");
}
}
2. 日期格式化工具
SimpleDateFormat 是线程不安全的,用 ThreadLocal 可以解决这个问题。
// 错误示例:多线程下会出问题
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) {
return sdf.format(date); // 多线程并发时会出错!
}
}
// 正确示例:使用ThreadLocal
public class ThreadSafeDateUtils {
// 每个线程都有自己的SimpleDateFormat实例
private static final ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return threadLocal.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
// 使用后清理(可选,因为DateFormat对象可以复用)
public static void clear() {
threadLocal.remove();
}
}
3. 分页信息管理
在分页查询中,ThreadLocal可以保存分页参数,在Service和Dao层都能获取。
public class PageContext {
private static final ThreadLocal<Integer> PAGE_NO = new ThreadLocal<>();
private static final ThreadLocal<Integer> PAGE_SIZE = new ThreadLocal<>();
public static void setPageInfo(Integer pageNo, Integer pageSize) {
PAGE_NO.set(pageNo);
PAGE_SIZE.set(pageSize);
}
public static Integer getPageNo() {
Integer pageNo = PAGE_NO.get();
return pageNo != null ? pageNo : 1; // 默认第一页
}
public static Integer getPageSize() {
Integer pageSize = PAGE_SIZE.get();
return pageSize != null ? pageSize : 20; // 默认20条
}
public static void clear() {
PAGE_NO.remove();
PAGE_SIZE.remove();
}
}
// 在Controller中使用
@GetMapping("/users")
public PageResult<User> getUsers(@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "20") Integer pageSize) {
try {
PageContext.setPageInfo(pageNo, pageSize);
return userService.getUserList();
} finally {
PageContext.clear();
}
}
4. 数据库连接管理
在一些框架中,为了确保同一个事务中使用同一个数据库连接,会用到 ThreadLocal。
public class ConnectionHolder {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = DataSourceUtils.getConnection();
connectionHolder.set(conn);
}
return conn;
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
public static void clearConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
DataSourceUtils.releaseConnection(conn);
}
connectionHolder.remove();
}
}
5. 全局参数传递
避免在方法调用链中层层传递相同参数,简化代码。
// 不用ThreadLocal,参数传递很痛苦
public void processOrder(String traceId, String userId, Order order) {
validateOrder(traceId, userId, order);
checkInventory(traceId, userId, order);
createLog(traceId, userId, order);
// ... 更多调用
}
// 使用ThreadLocal,清爽多了
public void processOrder(Order order) {
// 在入口处设置
TraceContext.setTraceId(UUID.randomUUID().toString());
UserContext.setUserId(getCurrentUserId());
validateOrder(order);
checkInventory(order);
createLog(order);
}
6. 分布式追踪ID传递
在微服务架构中,一个请求可能会经过多个服务,需要有一个traceId来追踪整个调用链。
@Component
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String traceId = httpRequest.getHeader("X-Trace-ID");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}
// 设置到ThreadLocal
TraceContext.setTraceId(traceId);
try {
// 在MDC中也设置,方便日志打印
MDC.put("traceId", traceId);
// 继续处理请求
chain.doFilter(request, response);
} finally {
// 一定要清理!!!
TraceContext.clear();
MDC.clear();
}
}
}
ThreadLocal 可能出现的问题
1. 内存泄漏(最重要的问题!)
这是ThreadLocal最容易被忽视,也是最危险的问题。我们先来看看为什么会内存泄漏。
public class MemoryLeakExample {
private static final ThreadLocal<BigObject> holder = new ThreadLocal<>();
public void process() {
// 设置一个大对象
holder.set(new BigObject()); // 假设BigObject占用100MB内存
// 执行业务逻辑...
// ...
// 忘记调用 holder.remove() 了!
}
}
问题:
- 当方法执行完后,
ThreadLocal对象本身(holder)可能被回收 - 但
ThreadLocalMap中,BigObject 这个 100MB 的对象仍然被引用着 - 如果这个线程是线程池中的线程,会被复用,永远不会被GC回收
- 随着请求增多,内存占用会越来越大,最终导致OOM
为什么会这样? 看一下 ThreadLocalMap 的内部结构:
static class ThreadLocalMap {
// Entry继承了WeakReference,key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // value是强引用!
}
private Entry[] table;
}
内存泄漏的两种情况:
1.ThreadLocal 对象被回收,但value还在
- key是弱引用,
ThreadLocal对象被回收后,key变成null - 但value是强引用,还在
Entry中被引用着 - 如果线程不结束,这个value就永远无法被回收
2.线程结束,但 ThreadLocalMap 还在
Thread对象有ThreadLocalMap的引用Thread结束,Thread 对象可以被回收- 但如果
ThreadLocal对象还被其他地方引用,就可能导致ThreadLocalMap无法被完全回收
2. 线程池中的值污染
在线程池环境下,线程会被复用。如果上一个任务设置了ThreadLocal值但没有清理,下一个任务可能会读到错误的值。
// 线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 任务1:用户A的操作
executor.execute(() -> {
UserContext.setUserId("userA");
// 执行业务逻辑...
// 忘记清理了!
});
// 任务2:用户B的操作
executor.execute(() -> {
// 这里可能获取到userA的ID!
String userId = UserContext.getUserId();
System.out.println("当前用户:" + userId); // 输出:userA
});
3. 父子线程值传递问题
默认情况下,子线程无法获取父线程的 ThreadLocal 值。
public class ParentChildExample {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
public static void main(String[] args) {
holder.set("父线程的值");
new Thread(() -> {
// 子线程获取不到父线程的值
System.out.println("子线程:" + holder.get()); // 输出:null
}).start();
}
}
ThreadLocal 问题的解决方案
方案1:一定要记得清理(最重要!)
黄金法则:每次使用完ThreadLocal,一定要调用remove()方法。
public class SafeUserContext {
private static final ThreadLocal<User> context = new ThreadLocal<>();
public static void set(User user) {
context.set(user);
}
public static User get() {
return context.get();
}
public static void clear() {
context.remove(); // 清理!
}
}
// 使用try-finally确保一定会清理
public void processRequest(HttpServletRequest request) {
try {
User user = parseUser(request);
SafeUserContext.set(user);
// 执行业务逻辑
doBusiness();
} finally {
// 无论是否发生异常,都会执行清理
SafeUserContext.clear();
}
}
方案2:使用拦截器/过滤器统一管理
在Web应用中,可以使用拦截器或过滤器统一管理ThreadLocal的生命周期。
@Component
public class ThreadLocalCleanInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 在请求开始时设置ThreadLocal
String traceId = request.getHeader("X-Trace-ID");
TraceContext.setTraceId(traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 请求结束后清理所有ThreadLocal
TraceContext.clear();
UserContext.clear();
PageContext.clear();
// ... 清理其他ThreadLocal
}
}
方案3:使用InheritableThreadLocal传递值
如果需要父子线程间传递值,可以使用InheritableThreadLocal。
public class InheritableContext {
private static final InheritableThreadLocal<String> context =
new InheritableThreadLocal<>();
public static void main(String[] args) {
context.set("父线程的值");
new Thread(() -> {
// 子线程可以获取到父线程的值
System.out.println("子线程:" + context.get()); // 输出:父线程的值
// 子线程修改值,不会影响父线程
context.set("子线程修改后的值");
}).start();
Thread.sleep(100);
System.out.println("父线程:" + context.get()); // 输出:父线程的值
}
}
注意:线程池场景下慎用,因为线程是复用的,不是新创建的。
方案4:使用阿里开源的 TransmittableThreadLocal
对于线程池场景,可以考虑使用阿里的TransmittableThreadLocal,它是InheritableThreadLocal的增强版。
<!-- 添加依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
public class TransmittableExample {
private static final TransmittableThreadLocal<String> context =
new TransmittableThreadLocal<>();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 使用TtlExecutors包装线程池
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);
context.set("value-1");
ttlExecutor.execute(() -> {
System.out.println("任务1:" + context.get()); // 输出:value-1
});
context.set("value-2");
ttlExecutor.execute(() -> {
System.out.println("任务2:" + context.get()); // 输出:value-2
});
}
}
方案5:ThreadLocal的封装
我们可以封装一个安全的ThreadLocal工具类。
public class SafeThreadLocal<T> {
private final ThreadLocal<T> threadLocal = new ThreadLocal<>();
// 设置值,并返回一个Cleanup对象
public Cleanup set(T value) {
threadLocal.set(value);
return new Cleanup();
}
public T get() {
return threadLocal.get();
}
public void remove() {
threadLocal.remove();
}
// Cleanup类,用于自动清理
public class Cleanup implements AutoCloseable {
@Override
public void close() {
threadLocal.remove();
}
}
}
// 使用示例:try-with-resources自动清理
public void process() {
SafeThreadLocal<String> safeHolder = new SafeThreadLocal<>();
try (SafeThreadLocal<String>.Cleanup cleanup = safeHolder.set("value")) {
// 执行业务逻辑
String value = safeHolder.get();
// ...
} // 这里会自动调用cleanup.close(),清理ThreadLocal
}
写在最后
ThreadLocal 是Java并发编程中的一个重要工具,它解决了多线程环境下的数据隔离问题。但任何强大的工具一样,它都需要被正确的使用。
该用ThreadLocal的时候:
- 需要在线程内共享数据,但不想用方法参数传递
- 需要保证线程安全的数据隔离
- 工具类需要线程安全但不想用同步锁
看到这,相信你对 ThreadLocal 已经不再是那么陌生了。
以上就是Java项目开发中ThreadLocal的6大用法总结的详细内容,更多关于Java ThreadLocal用法的资料请关注脚本之家其它相关文章!
