在子线程中,很多东西会失效或不可用,这是开发中非常容易踩的坑。我按影响从大到小给你列出来:
🔴 一、事务管理(@Transactional)完全失效
这是最大的坑!Spring 的@Transactional基于ThreadLocal存储数据库连接,子线程无法获取。
java
@Service 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、租户ID)
java
@RestController 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 InheritableThreadLocal<String> 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(); }🔴 三、异常处理:主线程捕获不到子线程异常
java
public 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(如日志、缓存、监控)可能失效:
java
@Service 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 用于关联同一请求的日志:
java
@RestController 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 Map<String, 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 -> { Map<String, 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(List<InspectionRemind> inspectionRemindList) { // 1. 在主线程中准备好数据(如果有事务,这里完成) SaveReq req = getVehicleMessageBatchSaveReq(getSaveMsgParams(inspectionRemindList)); // 2. 提交到线程池(并处理异常) executor.execute(() -> { try { // 3. 子线程只做无状态的远程调用 baseMessageFeignClient.batchSave(req); } catch (Exception e) { log.error("批量保存消息失败", e); // 4. 做补偿处理,比如记录失败日志或重试 } }); }如果子线程确实需要用户上下文信息(如当前登录用户),记得在主线程中提前提取出来再传入,而不是在子线程中去获取。