应重写threadpoolexecutor的beforeexecute和afterexecute方法或用threadfactory注入上下文以精准记录任务生命周期;前者在任务执行前后记录时间、线程名、异常等,后者通过自定义线程名和mdc传递追踪id;还可包装runnable实现任务级透传,同时规避日志性能与mdc传递陷阱。

要在线程池执行任务时记录日志,关键不是简单地在Runnable里加log.info(),而是从线程池调度源头介入——通过重写ThreadPoolExecutor的beforeExecute和afterExecute方法,或使用ThreadFactory统一注入上下文,才能稳定、可追溯地捕获任务生命周期。
用beforeExecute和afterExecute精准记录任务进出
这两个钩子方法由ThreadPoolExecutor提供,在任务真正执行前/后被调用,且运行在线程池的工作线程中,天然具备任务与线程的绑定关系。
-
beforeExecute:可记录任务提交时间、线程名、任务类型(如
getClass().getSimpleName()),甚至提取自定义注解(如@TaskName("订单超时检查")) -
afterExecute:除记录结束时间外,需检查
Throwable t参数——若不为null,说明任务抛出未捕获异常,此时应记录错误堆栈;若为null,代表正常完成 - 注意:这两个方法不处理拒绝策略(如
RejectedExecutionException),拒绝日志需在提交方(如submit()外围)单独捕获
用ThreadFactory为线程注入唯一标识,关联日志链路
默认线程名如pool-1-thread-2缺乏业务语义。自定义ThreadFactory可为每个线程设置带业务前缀的名称,并结合MDC(Mapped Diagnostic Context)传递追踪ID。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 创建线程时生成UUID或取当前时间戳+序号,设为线程名(如
"order-cleanup-20240520-001") - 在
newThread中初始化MDC.put("threadId", id),后续所有日志自动携带该字段 - 配合日志框架(如Logback)配置
%X{threadId},即可在每行日志中看到归属线程
包装Runnable/Callable实现任务级上下文透传
当任务本身是第三方代码或匿名类时,无法修改其内部逻辑。此时可借助装饰器模式,在提交前封装原始任务。
- 定义
TracedRunnable,构造时捕获提交时间、调用栈(Thread.currentThread().getStackTrace()前几帧)、业务标签等 - 在
run()中先记录“开始”,执行原任务,再捕获耗时并记录“结束”或异常 - 对
Future任务,建议用CompletableFuture.supplyAsync(() -> {...}, executor)替代submit,便于在回调中统一打点
避免常见陷阱
日志本身不应成为性能瓶颈或引发死锁。
- 日志内容避免调用可能阻塞的方法(如
toString()触发数据库查询) - 不要在
beforeExecute中做耗时操作(如远程调用),否则会拖慢线程池吞吐 - 若使用异步日志(如Logback的
AsyncAppender),确保MDC值能正确传递到异步线程——需显式调用MDC.getCopyOfContextMap()并在线程切换时恢复 - 高并发场景下,频繁打日志可能撑爆磁盘或影响GC,建议按业务等级分级(如仅记录ERROR和慢任务WARN)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










