有这么一段代码,
public class JobLogUtil { private static Logger logger = Logger.getLogger("SYSTEM"); /** * 消息级别 * @param modelType * @param node * @param logType * @param jobId * @param organizationId * @param message * @param remark */ public static void info(Integer modelType, String node, Integer logType, Integer jobId, Long organizationId, String message, String remark) { message=message.replace("'","\\'"); MDC.put("modelType", modelType); if (StringUtils.isBlank(node)) { MDC.put("node", ""); } else { MDC.put("node", node); } MDC.put("logType", logType); if (null == jobId) { MDC.put("jobId", 0); } else { MDC.put("jobId", jobId); } if (null == organizationId) { MDC.put("organizationId", 0); } else { MDC.put("organizationId", organizationId); } MDC.put("message", message); if (StringUtils.isBlank(remark)) { MDC.put("remark", ""); } else { MDC.put("remark", remark); } logger.info(message); }}
调用之后,整个程序卡住无法继续执行,非常奇怪,在本地测试没发现什么问题,但是在服务器上就卡死了,或者执行非常非常慢。
描述下整个流程, 执行某些任务,因为任务的量比较大,使用谷歌的
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(threadPoolExecutor);
构建了线程池,然后将任务全部提交到线程池中,
futures.add(executorService.submit(new Task())
执行完成后,
ListenableFuture<List<Task>> resultsFuture = Futures.successfulAsList(futures);
获取线程执行的结果。
阻塞就发生在Task 子线程任务中,为什么会发生卡死的问题?
首先来说下MDC是什么
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
看到上面大概就有一些想法了,MDC用到了ThreadLocal和哈希表map,这2个东西在多线程的环境下是比较常用的,但一旦用的不好,就是造成死锁,循环等等问题,特别是使用完后应该及时清理
Map map = MDC.getContext();
if(map != null) {
map.clear();
}
将上面MDC代码块去掉后,程序正常执行,说明就是MDC使用不当造成的问题。
在多线程环境下,尽量不要使用自己不熟悉的技术,以免造成问题而不知。
2021年12月17日14:43:58 补充
MDC采用Map的方式存储上下文,线程独立的,子线程会从父线程拷贝上下文。其调用方法以下:
-
保存信息到上下文
MDC.put(key, value);
-
从上下文获取设置的信息
MDC.get(key);
-
清楚上下文中指定的key的信息
MDC.remove(key);
-
清除全部
clear();
-
输出模板,注意是大写
[%X{key}]
log4j.appender.consoleAppender.layout.ConversionPattern = %-4r [%t] %5p %c %x - %m - %X{key}%n
在 log4j 1.x
中 MDC
的使用方式如下:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 填充数据 MDC.put(Contents.REQUEST_ID, UUID.randomUUID().toString()); chain.doFilter(request, response); } finally { // 请求结束时清除数据,否则会造成内存泄露问题 MDC.remove(Contents.REQUEST_ID); } }
没有释放,最终导致出问题
在 log4j 2.x
中,使用 ThreadContext
代替了 MDC
和 NDC
,使用方式如下:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 填充数据 ThreadContext.put(Contents.REQUEST_ID, UUID.randomUUID().toString()); chain.doFilter(request, response); } finally { // 请求结束时清除数据,否则会造成内存泄露问题 ThreadContext.remove(Contents.REQUEST_ID); } }