天天看點

【轉】建立線程以及線程池時候要指定與業務相關的名字,以便于追溯問題

原文:https://www.jianshu.com/p/d6245f2c3a9d

3.9 建立線程以及線程池時候要指定與業務相關的名字,以便于追溯問題

日常開發中當一個應用中需要建立多個線程或者線程池時候最好給每個線程或者線程池根據業務類型設定具體的名字,以便在出現問題時候友善進行定位,下面就通過執行個體來說明不設定時候為何難以定位問題,以及如何進行設定。

3.9.1建立線程需要帶線程名

下面通過簡單的代碼來說明不指定線程名稱為何難定位問題,代碼如下:

public static void main(String[] args) {
       //訂單子產品
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("儲存訂單的線程");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                throw new NullPointerException();
            }
        });
     //發貨子產品
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("儲存收獲位址的線程");
            }
        });

        threadOne.start();
        threadTwo.start();

    }      

如上代碼分别建立了線程one和線程two并且啟動執行運作上面代碼可能會輸出如下:

image.png

從運作接口可知

Thread-0

抛出了NPE異常,那麼單看這個日志根本無法判斷是訂單子產品的線程抛出的異常,首先我們分析下這個

Thread-0

是怎麼來的,這要看下建立線程時候的代碼:

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }      

可知如果調用了沒有指定線程名字的方法建立了線程,内部會使用

"Thread-" + nextThreadNum()

作為線程的預設名字,其中nextThreadNum代碼如下:

private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }
      

可知threadInitNumber是static變量,nextThreadNum是static方法,是以線程的編号是全應用唯一的并且是遞增的,另外這裡由于涉及到了多線程遞增threadInitNumber也就是執行讀取-遞增-寫入操作,而這個是線程不安全的是以使用了方法級别的synchronized進行同步。

當一個系統中有多個業務子產品而每個子產品中有都是用了自己的線程,除非抛出與業務相關的異常,否者比如上面抛出的NPE異常,根本沒法判斷是哪一個子產品出現了問題,現在修改代碼如下:

static final String THREAD_SAVE_ORDER = "THREAD_SAVE_ORDER";
    static final String THREAD_SAVE_ADDR = "THREAD_SAVE_ADDR";

    public static void main(String[] args) {
        // 訂單子產品
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("儲存訂單的線程");
                throw new NullPointerException();
            }
        }, THREAD_SAVE_ORDER);
        // 發貨子產品
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("儲存收貨位址的線程");
            }
        }, THREAD_SAVE_ADDR);

        threadOne.start();
        threadTwo.start();

    }      

如上代碼在建立線程的時候給線程指定了一個與具體業務子產品相關的名字,下面運作結果輸出為:

從運作結果就可以定位到是儲存訂單子產品抛出了NPE異常,一下子就可以定位到問題。

3.9.2建立線程池時候也需要指定線程池的名稱

同理下面通過簡單的代碼來說明不指定線程池名稱為何難定位問題,代碼如下:

static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

    public static void main(String[] args) {

        //接受使用者連結子產品
        executorOne.execute(new  Runnable() {
            public void run() {
                System.out.println("接受使用者連結線程");
                throw new NullPointerException();
            }
        });
        //具體處理使用者請求子產品
        executorTwo.execute(new  Runnable() {
            public void run() {
                System.out.println("具體處理業務請求線程");
            }
        });
        
        executorOne.shutdown();
        executorTwo.shutdown();
    }      

運作代碼輸出如下結果:

同理我們并不知道是那個子產品的線程池抛出了這個異常,那麼我們看下這個

pool-1-thread-1

是如何來的。其實是使用了線程池預設的ThreadFactory,翻看線程池建立的源碼如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
    
   public static ThreadFactory defaultThreadFactory() {
   return new DefaultThreadFactory();
    }
    

 static class DefaultThreadFactory implements ThreadFactory {
        //(1)
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        //(2)
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        //(3)
        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) {
           //(4)
            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;
        }
    }      

如上代碼DefaultThreadFactory的實作可知:

  • 代碼(1)poolNumber是static的原子變量用來記錄目前線程池的編号是應用級别的,所有線程池公用一個,比如建立第一個線程池時候線程池編号為1,建立第二個線程池時候線程池的編号為2,這裡

    pool-1-thread-1

    裡面的pool-1中的1就是這個值
  • 代碼(2)threadNumber是線程池級别的,每個線程池有一個該變量用來記錄該線程池中線程的編号,這裡

    pool-1-thread-1

    裡面的thread-1中的1就是這個值
  • 代碼(3)namePrefix是線程池中線程的字首,預設固定為pool
  • 代碼(4)具體建立線程,可知線程的名稱使用

    namePrefix + threadNumber.getAndIncrement()

    拼接的。

從上知道我們隻需對實作ThreadFactory并對DefaultThreadFactory的代碼中namePrefix的初始化做手腳,當需要建立線程池是傳入與業務相關的namePrefix名稱就可以了,代碼如下:

// 命名線程工廠
    static class NamedThreadFactory 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;

        NamedThreadFactory(String name) {

            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
            if (null == name || name.isEmpty()) {
                name = "pool";
            }

            namePrefix = name + "-" + 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;
        }
    }      

然後建立線程池時候如下:

static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-ACCEPT-POOL"));
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-PROCESS-POOL"));      

然後運作執行結果如下:

ASYN-ACCEPT-POOL-1-thread-1

就可以知道是接受連結線程池抛出的異常。

3.9.3總結

本節通過簡單的例子介紹了為何不給線程或者線程池起名字會給問題排查帶來麻煩,然後通過源碼原理介紹線程和線程池名稱是預設名稱是如何來的,以及如何自定義線程池名稱,以便問題追溯。