Java的每個Thread都希望擁有自己的名稱

尋覓beyond發表於2020-05-23

一. 介紹

  平時工作中可能會碰到排查多執行緒的bug,而在排查的時候,如果執行緒(單個執行緒或者是執行緒池的執行緒)沒有一個比較明確的名稱,那麼在排查的時候就比較頭疼,因為排查問題首先需要找出“問題執行緒”,如果連“問題執行緒”都找不到,就很難找出問題原因,本文就針對多執行緒中涉及到的執行緒池、執行緒組、執行緒名稱,介紹如果對其進行設定名稱,方便排查問題時快速定位。

 

二. 設定執行緒名稱

2.1 使用Thread+Runnable介面形式

  如果是使用實現Runnable介面,然後使用Thread構造器來直接建立執行緒時,有兩種方式設定執行緒名稱:

  1.在呼叫Thread的構造器時,傳入第二個引數即可,構造器定義如下

Thread Thread(Runnable target, String threadName)

  2.呼叫Thread物件的setName方法,設定執行緒名稱即可;

  上面兩種方法的示例程式碼如下:

package cn.ganlixin;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

@Slf4j
public class DefineThreadName {

    /**
     * 不設定執行緒名稱,使用預設的執行緒名稱
     */
    @Test
    public void defaultThreadName() {
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            String threadGroupName = Thread.currentThread().getThreadGroup().getName();
            long threadId = Thread.currentThread().getId();
            log.info("threadName:{}, threadGroupName:{}, threadId:{}", threadName, threadGroupName, threadId);
        }).start();
        // 輸出 threadName:Thread-1, threadGroupName:main, threadId:13
    }

    /**
     * 自定義執行緒的名稱
     */
    @Test
    public void customThreadName() {
        // 方式一:指定Thread構造方法的第二個引數,也就是執行緒的名稱
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            String threadGroupName = Thread.currentThread().getThreadGroup().getName();
            long threadId = Thread.currentThread().getId();
            log.info("threadName:{}, threadGroupName:{}, threadId:{}", threadName, threadGroupName, threadId);
        }, "my-custom-thread-name-1").start();

        // 輸出 threadName:my-custom-thread-name-1, threadGroupName:main, threadId:13

        // 方式二:使用Thread物件的setName方法,設定執行緒名稱
        Thread thread = new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            String threadGroupName = Thread.currentThread().getThreadGroup().getName();
            long threadId = Thread.currentThread().getId();
            log.info("threadName:{}, threadGroupName:{}, threadId:{}", threadName, threadGroupName, threadId);
        });
        thread.setName("my-custom-thread-name-2");
        thread.start();
        // 輸出 threadName:my-custom-thread-name-2, threadGroupName:main, threadId:14
    }
}

  

2.2 繼承Thread類的形式

  如果是繼承Thread類,那麼可以在子類中呼叫Thread僅接受一個字串作為執行緒名稱的構造器,像下面這麼做:

package cn.ganlixin;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

@Slf4j
public class DefineThreadName {

    /**
     * 自定義的繼承自Thread的執行緒類
     */
    private static class MyThread extends Thread {
        private MyThread(String threadName) {
            super(threadName); // Thread有一個構造器接收一個字串型別的引數,作為執行緒名稱
        }

        @Override
        public void run() {
            // 因為繼承自Thread,所以下面可以直接呼叫這些方法,而不需要通過Thread.currentThread()獲取當前執行緒
            String threadName = getName();
            String threadGroupName = getThreadGroup().getName();
            long threadId = getId();
            log.info("threadName:{}, threadGroupName:{}, threadId:{}", threadName, threadGroupName, threadId);
        }
    }

    /**
     * 測試設定、更改執行緒名稱
     */
    @Test
    public void testInheritThread() {
        MyThread t1 = new MyThread("my-extends-thread-name-1");
        t1.start();
        // 輸出 threadName:my-extends-thread-name-1, threadGroupName:main, threadId:13

        MyThread t2 = new MyThread("my-extends-thread-name-2");
        t2.setName("changed-thread-name"); // 手動修改執行緒名稱
        t2.start();
        // 輸出 threadName:changed-thread-name, threadGroupName:main, threadId:14
    }
}

  

三. 設定執行緒組的名稱

  執行緒組名稱需要在建立執行緒組的時候進行指定,然後使用執行緒組的時候將執行緒組作為Thread類的構造器引數傳入即可,示例程式碼如下:

package cn.ganlixin.name;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

@Slf4j
public class ThreadGroupName {

    @Test
    public void defineThreadGroupName() {
        // 定義一個執行緒組,傳入執行緒組的名稱(自定義)
        ThreadGroup threadGroup = new ThreadGroup("my-thread-group-name");

        Runnable runnable = () -> {
            String threadGroupName = Thread.currentThread().getThreadGroup().getName();
            String threadName = Thread.currentThread().getName();
            long threadId = Thread.currentThread().getId();

            log.info("threadGroupName:{}, threadName:{}, threadId:{}", threadGroupName, threadName, threadId);
        };

        Thread t1 = new Thread(threadGroup, runnable);
        t1.start();
        // 輸出 threadGroupName:my-thread-group-name, threadName:Thread-1, threadId:13

        // 第三個引數是執行緒名稱
        Thread t2 = new Thread(threadGroup, runnable, "my-thread-name");
        t2.start();
        // threadGroupName:my-thread-group-name, threadName:my-thread-name, threadId:14
    }
}

  

四. 設定執行緒池名稱

4.1 建立執行緒池的兩種途徑

  建立執行緒池,有兩種方式: 

  1.例項化ThreadPoolExecutor來建立執行緒池,可以指定相關的引數,方法定義如下:

  

  2.使用Executors的靜態方法建立執行緒池,實際是對ThreadPoolExecutor物件的建立過程進行了封裝,可用的方法定義如下:

  

  上面的諸多定義中,提到了一個ThreadFactory,“執行緒工廠”,這是一個介面,定義了建立執行緒的統一規範,實現類需要重寫newThread方法,定義如下:

package java.util.concurrent;

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

  當我們呼叫Executors或者使用ThreadPoolExecutor來建立執行緒池,如果沒有指定ThreadFactory,那麼就會使用預設的Executors.DefaultThreadFactory

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;
    }
}

  如果我們要對執行緒池中的執行緒建立進行擴充套件,那麼實現ThreadFactory介面,加入自己的擴充套件即可,此處對於執行緒池中執行緒的名稱進行設定,也是可以在這裡實現。

  

4.2 自定義執行緒工廠(ThreadFactory)

  自己實現ThreadFactory介面,可以參考Executors.DefaultThreadFactory,做一下細微的修改就行了,下面是我建立的NameableThreadFactory,意為“可命名的執行緒工廠”:

package cn.ganlixin.name;

import org.apache.commons.lang3.StringUtils;

import java.util.Objects;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 描述:
 * 參照Executors.DefaultThreadFactory,自定義ThreadFactory實現類
 *
 * @author ganlixin
 * @create 2020-05-23
 */
public class NameableThreadFactory implements ThreadFactory {
    /**
     * 對執行緒池的數量進行計數,注意是類屬性
     */
    private static final AtomicInteger poolNumber = new AtomicInteger(1);

    /**
     * 執行緒組名稱
     */
    private ThreadGroup group;

    /**
     * 對執行緒池中的執行緒資料進行計數,注意是例項屬性
     */
    private final AtomicInteger threadNumber = new AtomicInteger(1);

    /**
     * 執行緒名稱的字首
     */
    private String namePrefix;

    /**
     * Executors.DefaultThreadFactory中預設的方式(設定執行緒組、執行緒名稱字首)
     */
    public NameableThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }

    /**
     * 建立執行緒工廠,指定執行緒名稱字首
     *
     * @param threadNamePrefix 執行緒名稱字首
     */
    public NameableThreadFactory(String threadNamePrefix) {
        if (StringUtils.isBlank(threadNamePrefix)) {
            throw new IllegalArgumentException("執行緒名稱的字首不能為空");
        }

        // 執行緒組,仍舊使用舊規則
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();

        // 指定執行緒的名稱字首,設定為傳入的名稱字首
        this.namePrefix = threadNamePrefix + "-";
    }

    /**
     * 建立執行緒工廠,指定執行緒組、以及執行緒名稱字首
     *
     * @param threadGroup      執行緒組例項
     * @param threadNamePrefix 執行緒名稱字首
     */
    public NameableThreadFactory(ThreadGroup threadGroup, String threadNamePrefix) {
        if (Objects.isNull(threadGroup)) {
            throw new IllegalArgumentException("執行緒組不能為空");
        }

        if (StringUtils.isBlank(threadNamePrefix)) {
            throw new IllegalArgumentException("執行緒名稱的字首不能為空");
        }

        this.group = threadGroup;
        this.namePrefix = threadNamePrefix + "-";
    }

    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;
    }
}

  進行測試,因為Executors和ThreadPoolExecutor的本質是一樣的,所以這裡使用Executors進行測試,只需要在用到ThreadFactory的時候,引入自己的建立NameableThreadFactory即可:

package cn.ganlixin.name;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class TestNameableThreadFactory {

    @Test
    public void test() throws InterruptedException {
        ExecutorService executorService1 = Executors.newFixedThreadPool(3, new NameableThreadFactory("自定義執行緒池one"));

        Runnable runnable = () -> {
            String threadGroupName = Thread.currentThread().getThreadGroup().getName();
            String threadName = Thread.currentThread().getName();
            long threadId = Thread.currentThread().getId();
            log.info("threadGroupName:{}, threadName:{}, threadId:{}", threadGroupName, threadName, threadId);
        };

        for (int i = 0; i < 3; i++) {
            executorService1.submit(runnable);
        }
        // 輸出
        // threadGroupName:main, threadName:自定義執行緒池one-1, threadId:14
        // threadGroupName:main, threadName:自定義執行緒池one-2, threadId:15
        // threadGroupName:main, threadName:自定義執行緒池one-3, threadId:16
        Thread.sleep(100);

        // 建立執行緒組
        ThreadGroup threadGroup = new ThreadGroup("自定義執行緒組one");
        ExecutorService executorService2 = Executors.newFixedThreadPool(3, new NameableThreadFactory(threadGroup, "自定義執行緒池two"));
        for (int i = 0; i < 3; i++) {
            executorService2.submit(runnable);
        }
        // 輸出:
        // threadGroupName:自定義執行緒組one, threadName:自定義執行緒池two-1, threadId:16
        // threadGroupName:自定義執行緒組one, threadName:自定義執行緒池two-2, threadId:17
        // threadGroupName:自定義執行緒組one, threadName:自定義執行緒池two-3, threadId:18

        Thread.sleep(1000);
    }
}

  

 

相關文章