在子线程中会失效的情况 📅 2026/7/11 22:02:12 在子线程中很多东西会失效或不可用这是开发中非常容易踩的坑。我按影响从大到小给你列出来 一、事务管理Transactional完全失效这是最大的坑Spring 的Transactional基于ThreadLocal存储数据库连接子线程无法获取。javaService public class OrderService { Autowired private OrderMapper orderMapper; Transactional // ❌ 这个事务对子线程无效 public void createOrder() { // 主线程开启事务获取数据库连接 orderMapper.insert(order); new Thread(() - { // ❌ 子线程这里没有事务 // 每条SQL都是独立提交的一条失败不会回滚之前的 orderMapper.updateStock(productId); orderMapper.insertLog(创建订单); }).start(); // 主线程事务提交 } }解决方案java// 方案1将子线程的操作提前到主线程事务内完成 Transactional public void createOrder() { orderMapper.insert(order); // 所有数据库操作都在主线程完成 orderMapper.updateStock(productId); orderMapper.insertLog(创建订单); // 最后再异步发消息通知不影响事务 asyncSendNotification(); } // 方案2使用 Transactional 的传播机制让子线程开启新事务 Transactional(propagation Propagation.REQUIRES_NEW) public void updateStockInNewTransaction() { // 这个方法会在新事务中执行 } 二、ThreadLocal 数据丢失Spring 的很多上下文依赖ThreadLocal子线程无法访问java// 常见的 ThreadLocal 数据 - RequestContextHolder当前请求的 HttpServletRequest/Response - SecurityContextHolder当前用户的认证信息 - TransactionSynchronizationManager事务同步 - 自定义的 ThreadLocal如用户ID、租户IDjavaRestController public class UserController { GetMapping(/user) public String getUser() { // 主线程从 SecurityContextHolder 获取当前用户 Authentication auth SecurityContextHolder.getContext().getAuthentication(); String username auth.getName(); // 张三 new Thread(() - { // ❌ 子线程SecurityContextHolder 是空的 Authentication subAuth SecurityContextHolder.getContext().getAuthentication(); // subAuth null System.out.println(子线程用户: subAuth); // null }).start(); return ok; } }解决方案使用InheritableThreadLocal或在创建线程时传递上下文java// 方案1使用 InheritableThreadLocal子线程继承父线程的值 private static InheritableThreadLocalString userContext new InheritableThreadLocal(); // 方案2手动传递推荐更可控 GetMapping(/user) public String getUser() { Authentication auth SecurityContextHolder.getContext().getAuthentication(); // 在子线程执行前捕获上下文 new Thread(() - { // 手动设置到子线程 SecurityContextHolder.getContext().setAuthentication(auth); // 现在可以正常使用了 System.out.println(SecurityContextHolder.getContext().getAuthentication().getName()); }).start(); } 三、异常处理主线程捕获不到子线程异常javapublic void test() { try { new Thread(() - { int i 1 / 0; // 抛出 ArithmeticException }).start(); // ❌ 主线程的 try-catch 捕获不到子线程异常 } catch (Exception e) { // 这里的代码永远不会执行 System.out.println(捕获异常: e); } }解决方案java// 方案1在子线程内部处理异常 new Thread(() - { try { int i 1 / 0; } catch (Exception e) { log.error(子线程执行失败, e); } }).start(); // 方案2使用 UncaughtExceptionHandler Thread thread new Thread(() - { int i 1 / 0; }); thread.setUncaughtExceptionHandler((t, e) - { log.error(线程 {} 异常: , t.getName(), e); }); thread.start(); // 方案3使用 Future 捕获异常推荐 Future? future executor.submit(() - { int i 1 / 0; }); try { future.get(); // 这里会抛出 ExecutionException } catch (ExecutionException e) { Throwable cause e.getCause(); // 获取真正的异常 log.error(任务执行异常, cause); } 四、Spring AOP / 代理失效子线程中调用 Spring Bean 的方法AOP如日志、缓存、监控可能失效javaService public class MessageService { Autowired private MessageService self; // 注入自身代理对象 Cacheable(messages) // 缓存切面 public String getMessage(Long id) { return message_ id; } public void test() { new Thread(() - { // ❌ 直接调用缓存切面不会生效除非通过代理调用 String msg getMessage(1L); // ✅ 通过代理调用切面生效 String msg2 self.getMessage(1L); }).start(); } } 五、日志 MDC诊断上下文丢失日志框架的 MDC 用于关联同一请求的日志javaRestController public class OrderController { GetMapping(/order) public String createOrder() { // 主线程设置 traceId MDC.put(traceId, UUID.randomUUID().toString()); log.info(主线程开始处理); new Thread(() - { // ❌ 子线程MDC 是空的traceId 丢失 log.info(子线程处理); // 日志里没有 traceId }).start(); return ok; } }解决方案java// 方案1手动传递 MDC MapString, String contextMap MDC.getCopyOfContextMap(); new Thread(() - { if (contextMap ! null) { MDC.setContextMap(contextMap); } log.info(子线程处理); // 现在有 traceId 了 }).start(); // 方案2使用包装的线程池Spring 的 TaskDecorator Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable - { MapString, String contextMap MDC.getCopyOfContextMap(); return () - { try { MDC.setContextMap(contextMap); runnable.run(); } finally { MDC.clear(); } }; }); return executor; } 六、数据库连接/资源java// ❌ 错误示例子线程复用主线程的连接 public void test() { Connection conn dataSource.getConnection(); // 主线程获取连接 new Thread(() - { // 危险Connection 不是线程安全的 conn.prepareStatement(...).execute(); }).start(); } // ✅ 正确每个线程独立获取连接 new Thread(() - { try (Connection conn dataSource.getConnection()) { // 子线程自己获取连接 conn.prepareStatement(...).execute(); } }).start(); 七、部分失效的类加载器大多数情况下子线程能访问父线程的类加载器但如果使用了自定义类加载器如热部署环境可能失效。 总结对照表失效项失效程度解决方案Transactional 事务 完全失效主线程完成DB操作或使用新事务ThreadLocal 数据 完全丢失InheritableThreadLocal或手动传递异常捕获 主线程捕获不到子线程内部处理 或Future.get()Spring AOP 可能失效通过代理对象调用日志 MDC 丢失手动复制上下文数据库连接 非线程安全每个线程独立获取Request/Session 完全丢失提前提取参数传入子线程 最佳实践总结对于你的 Feign 调用场景建议java// ✅ 推荐将必要数据提前取出传入子线程 public void saveMessages(ListInspectionRemind inspectionRemindList) { // 1. 在主线程中准备好数据如果有事务这里完成 SaveReq req getVehicleMessageBatchSaveReq(getSaveMsgParams(inspectionRemindList)); // 2. 提交到线程池并处理异常 executor.execute(() - { try { // 3. 子线程只做无状态的远程调用 baseMessageFeignClient.batchSave(req); } catch (Exception e) { log.error(批量保存消息失败, e); // 4. 做补偿处理比如记录失败日志或重试 } }); }如果子线程确实需要用户上下文信息如当前登录用户记得在主线程中提前提取出来再传入而不是在子线程中去获取。