有這麼一段代碼,
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); } }