Java Review - 创建线程和线程池时建议指定与业务相关的名称
文章目錄
- 概述
- 線程
- 不指定線程名稱為何難定位問題
- Thread默認的線程名稱
- 指定線程名稱
- 線程池
- 不指定線程池名稱為何難定位問題
- 指定線程名稱
- 自定義線程名稱
- 小結
概述
在日常開發中,當在一個應用中需要創建多個線程或者線程池時最好給每個線程或者線程池根據業務類型設置具體的名稱,以便在出現問題時方便進行定位。
下面就通過實例來說明不設置為何難以定位問題,以及如何進行設置。
線程
不指定線程名稱為何難定位問題
import java.util.concurrent.TimeUnit;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/20 12:09* @mark: show me the code , change the world*/ public class ThreadWithName {public static void main(String[] args) {Thread t1 = new Thread(() -> System.out.println("模塊A開始處理業務"));Thread t2 = new Thread(() -> {// 模擬業務System.out.println("模塊B開始處理業務");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 模擬異常throw new NullPointerException();});t1.start();t2.start();} }如上代碼分別創建了t1和t2,運行上面的代碼, 【輸出結果】
Thread默認的線程名稱
從運行結果可知,Thread-1拋出了NPE異常,那么單看這個日志根本無法判斷是哪個模塊的的線程拋出的異常。首先我們分析下這個Thread-1是怎么來的,我們看一下創建線程時的代碼。
/*** Allocates a new {@code Thread} object. This constructor has the same* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}* {@code (null, target, gname)}, where {@code gname} is a newly generated* name. Automatically generated names are of the form* {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.** @param target* the object whose {@code run} method is invoked when this thread* is started. If {@code null}, this classes {@code run} method does* nothing.*/public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);}嘿 看到了嗎 "Thread-" + nextThreadNum()
/* For autonumbering anonymous threads. */private static int threadInitNumber;private static synchronized int nextThreadNum() {return threadInitNumber++;}由此可知,threadInitNumber是static變量,nextThreadNum是static方法,所以線程的編號是全應用唯一的并且是遞增的。
因為涉及多線程遞增threadInitNumber,也就是執行讀取—遞增—寫入操作,而這是線程不安全的,所以要使用方法級別的synchronized進行同步。
當一個系統中有多個業務模塊而每個模塊又都使用自己的線程時,除非拋出與業務相關的異常,否則你根本沒法判斷是哪一個模塊出現了問題?,F在修改代碼如下。
指定線程名稱
如上代碼在創建線程時給線程指定了一個與具體業務模塊相關的名稱,運行代碼,輸出結果為
從運行結果就可以定位到是模塊B拋出了NPE異常,一下子就可以找到問題所在。
線程池
不指定線程池名稱為何難定位問題
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/20 12:09* @mark: show me the code , change the world*/ public class ThreadPoolWithName {public static void main(String[] args) {ThreadPoolExecutor tpe1 = new ThreadPoolExecutor(5,5,10,TimeUnit.MINUTES,new LinkedBlockingDeque<>());ThreadPoolExecutor tpe2 = new ThreadPoolExecutor(5,5,10,TimeUnit.MINUTES,new LinkedBlockingDeque<>());tpe1.execute(()->System.out.println("模塊A 執行業務"));tpe2.execute(()->{System.out.println("模塊B 執行業務");// 模擬業務異常throw new NullPointerException();});tpe1.shutdown();tpe2.shutdown();} }運行代碼,輸出結果如下
加粗樣式
同樣,我們并不知道是哪個模塊的線程池拋出了這個異常,那么我們看下這個pool-2-thread-1是如何來的。
指定線程名稱
其實這里使用了線程池默認的ThreadFactory,查看線程池創建的源碼如下
/*** Creates a new {@code ThreadPoolExecutor} with the given initial* parameters and default thread factory and rejected execution handler.* It may be more convenient to use one of the {@link Executors} factory* methods instead of this general purpose constructor.** @param corePoolSize the number of threads to keep in the pool, even* if they are idle, unless {@code allowCoreThreadTimeOut} is set* @param maximumPoolSize the maximum number of threads to allow in the* pool* @param keepAliveTime when the number of threads is greater than* the core, this is the maximum time that excess idle threads* will wait for new tasks before terminating.* @param unit the time unit for the {@code keepAliveTime} argument* @param workQueue the queue to use for holding tasks before they are* executed. This queue will hold only the {@code Runnable}* tasks submitted by the {@code execute} method.* @throws IllegalArgumentException if one of the following holds:<br>* {@code corePoolSize < 0}<br>* {@code keepAliveTime < 0}<br>* {@code maximumPoolSize <= 0}<br>* {@code maximumPoolSize < corePoolSize}* @throws NullPointerException if {@code workQueue} is null*/public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);}Here we go -------------> Executors.defaultThreadFactory()
public static ThreadFactory defaultThreadFactory() {return new DefaultThreadFactory();} /*** The default thread factory*/static class DefaultThreadFactory implements ThreadFactory {private static final AtomicInteger poolNumber = new AtomicInteger(1);private final ThreadGroup group;private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();namePrefix = "pool-" +poolNumber.getAndIncrement() +"-thread-";}public Thread newThread(Runnable r) {Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon())t.setDaemon(false);if (t.getPriority() != Thread.NORM_PRIORITY)t.setPriority(Thread.NORM_PRIORITY);return t;}}-
poolNumber是static的原子變量,用來記錄當前線程池的編號,它是應用級別的,所有線程池共用一個,比如創建第一個線程池時線程池編號為1,創建第二個線程池時線程池的編號為2,所以pool-2-thread-1里面的pool-1中的1就是這個值
-
threadNumber是線程池級別的,每個線程池使用該變量來記錄該線程池中線程的編號,所以pool-2-thread-1里面的thread-1中的1就是這個值。
-
namePrefix是線程池中線程名稱的前綴,默認固定為pool。
-
具體創建線程,線程的名稱是使用namePrefix + threadNumber.getAndIncrement()拼接的
自定義線程名稱
由此我們知道,只需對DefaultThreadFactory的代碼中的namePrefix的初始化做下手腳,即當需要創建線程池時傳入與業務相關的namePrefix名稱就可以了
我們看下hutool中是如何封裝的
import java.lang.Thread.UncaughtExceptionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger;import cn.hutool.core.util.StrUtil;/*** 線程創建工廠類,此工廠可選配置:* * <pre>* 1. 自定義線程命名前綴* 2. 自定義是否守護線程* </pre>* * @author looly* @since 4.0.0*/ public class NamedThreadFactory implements ThreadFactory {/** 命名前綴 */private final String prefix;/** 線程組 */private final ThreadGroup group;/** 線程組 */private final AtomicInteger threadNumber = new AtomicInteger(1);/** 是否守護線程 */private final boolean isDaemon;/** 無法捕獲的異常統一處理 */private final UncaughtExceptionHandler handler;/*** 構造* * @param prefix 線程名前綴* @param isDaemon 是否守護線程*/public NamedThreadFactory(String prefix, boolean isDaemon) {this(prefix, null, isDaemon);}/*** 構造* * @param prefix 線程名前綴* @param threadGroup 線程組,可以為null* @param isDaemon 是否守護線程*/public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon) {this(prefix, threadGroup, isDaemon, null);}/*** 構造* * @param prefix 線程名前綴* @param threadGroup 線程組,可以為null* @param isDaemon 是否守護線程* @param handler 未捕獲異常處理*/public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDaemon, UncaughtExceptionHandler handler) {this.prefix = StrUtil.isBlank(prefix) ? "Hutool" : prefix;if (null == threadGroup) {threadGroup = ThreadUtil.currentThreadGroup();}this.group = threadGroup;this.isDaemon = isDaemon;this.handler = handler;}@Overridepublic Thread newThread(Runnable r) {final Thread t = new Thread(this.group, r, StrUtil.format("{}{}", prefix, threadNumber.getAndIncrement()));//守護線程if (false == t.isDaemon()) {if (isDaemon) {// 原線程為非守護則設置為守護t.setDaemon(true);}} else if (false == isDaemon) {// 原線程為守護則還原為非守護t.setDaemon(false);}//異常處理if(null != this.handler) {t.setUncaughtExceptionHandler(handler);}//優先級if (Thread.NORM_PRIORITY != t.getPriority()) {// 標準優先級t.setPriority(Thread.NORM_PRIORITY);}return t;}}測試一下
從業務B-1就可以知道,這是接受用戶鏈接線程池拋出的異常。
小結
-
我們這里介紹了為何不為線程或者線程池起名字會給問題排查帶來麻煩,然后通過源碼分析介紹了線程和線程池名稱及默認名稱是如何來的,以及如何定義線程池名稱以便追溯問題。
-
另外,在run方法內使用try-catch塊,避免將異常拋到run 方法之外,同時打印日志也是一個最佳實踐。
總結
以上是生活随笔為你收集整理的Java Review - 创建线程和线程池时建议指定与业务相关的名称的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - Linked
- 下一篇: Java Review - 线程池资源一