java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java ThreadLocal用法

Java项目开发中ThreadLocal的6大用法总结

作者:程序员大华

ThreadLocal在项目中非常常用,本文整理了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 只是简单的把变量复制到每个线程,其实不是这样。

它的原理是这样的:

可以想象成每个线程都有一个小本本(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() 了!
    }
}

问题

为什么会这样? 看一下 ThreadLocalMap 的内部结构:

static class ThreadLocalMap {
    // Entry继承了WeakReference,key是弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value; // value是强引用!
    }
    
    private Entry[] table;
}

内存泄漏的两种情况:

1.ThreadLocal 对象被回收,但value还在

2.线程结束,但 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用法的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文