宣威市住房与城乡建设局网站,买购网十大品牌网,网页设计排版布局图片,被墙域名黑别人网站前言你的系统需要记录每个用户的操作日志#xff0c;包括用户ID、操作时间、操作内容等。在单线程环境下#xff0c;这很简单#xff0c;一个全局变量就够了。但到了Web应用中#xff0c;一个请求一个线程#xff0c;多个用户同时操作#xff0c;怎么保证A用户的操作不会…前言你的系统需要记录每个用户的操作日志包括用户ID、操作时间、操作内容等。在单线程环境下这很简单一个全局变量就够了。但到了Web应用中一个请求一个线程多个用户同时操作怎么保证A用户的操作不会被记录成B用户呢你可能会想到在每个方法里都传递用户信息但这样太麻烦了。这就是ThreadLocal要解决的核心问题在多线程环境下如何在不传递参数的情况下让同一个线程内的多个方法共享数据同时又不会影响其他线程ThreadLocal是什么简单理解ThreadLocal是一个让每个线程拥有自己独立变量副本的容器。对同一个ThreadLocal实例的get()/set()不同线程看到的是不同的数据互不干扰。看个最简单的例子public class UserContext { // 创建一个ThreadLocal变量用来存储用户ID private static final ThreadLocalString 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只是简单的把变量复制到每个线程其实不是这样。它的原理是这样的1. 每个线程Thread对象内部都有一个特殊的Map叫做ThreadLocalMap。2. 当你调用threadLocal.set(value)时实际上是把 value 放到了当前线程的ThreadLocalMap中key 是这个 ThreadLocal 对象。3. 当你调用threadLocal.get()时实际上是去当前线程的 ThreadLocalMap 中找这个 ThreadLocal 对象对应的 value。可以想象成每个线程都有一个小本本ThreadLocalMap这个小本本上记录着各种ThreadLocal变量的值。当你调用某个ThreadLocal的 get 方法就是在小本本上查找对应的内容。ThreadLocal 使用场景和示例1. 用户会话管理最常用在Web开发中这是最经典的使用场景。用户登录后将用户信息存入ThreadLocal在后续的任何地方都能获取。// 用户上下文工具类 public class UserContext { private static final ThreadLocalUserInfo 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 ThreadLocalSimpleDateFormat 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 ThreadLocalInteger PAGE_NO new ThreadLocal(); private static final ThreadLocalInteger 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 PageResultUser 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 ThreadLocalConnection 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 ThreadLocalBigObject 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继承了WeakReferencekey是弱引用 static class Entry extends WeakReferenceThreadLocal? { 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 ThreadLocalString 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 ThreadLocalUser 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 InheritableThreadLocalString 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 groupIdcom.alibaba/groupId artifactIdtransmittable-thread-local/artifactId version2.14.2/version /dependencypublic class TransmittableExample { private static final TransmittableThreadLocalString 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 }); } }方案5ThreadLocal的封装我们可以封装一个安全的ThreadLocal工具类。public class SafeThreadLocalT { private final ThreadLocalT 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() { SafeThreadLocalString safeHolder new SafeThreadLocal(); try (SafeThreadLocalString.Cleanup cleanup safeHolder.set(value)) { // 执行业务逻辑 String value safeHolder.get(); // ... } // 这里会自动调用cleanup.close()清理ThreadLocal }写在最后ThreadLocal是Java并发编程中的一个重要工具它解决了多线程环境下的数据隔离问题。但任何强大的工具一样它都需要被正确的使用。该用ThreadLocal的时候1. 需要在线程内共享数据但不想用方法参数传递2. 需要保证线程安全的数据隔离3. 工具类需要线程安全但不想用同步锁看到这相信你对ThreadLocal已经不再是那么陌生了。