侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 377 篇文章
  • 累计创建 136 个标签
  • 累计收到 12 条评论

目 录CONTENT

文章目录

给线程池自定义线程名字,方便问题回溯

孔子说JAVA
2022-12-22 / 0 评论 / 0 点赞 / 93 阅读 / 12,264 字 / 正在检测是否收录...

在日常开发项目中,我们一般都会创建多个线程池用来资源隔离,如果没有给线程池指定名称的话,万一线程抛出异常了,日志记录将无法定位到底是哪个线程池抛出的异常;所以为了方便排查,给线程池的线程自定义命名,主要方法就是重写生产线程工厂的命名方法。

1f241d546f682d45067525e888dd4437

1、ThreadFactory 线程工厂

线程池统一通过ThreadFactory创建新线程,可以说是工厂模式的应用。默认使用Executors.defaultThreadFactory工厂,该工厂创建的线程全部位于同一个ThreadGroup中,并且具有 pool-N-thread-M 的线程命名(N表示线程池工厂编号,M表示一个工厂创建的线程编号,都是自增的)和非守护进程状态。

  • 通过提供不同的ThreadFactory,您可以更改线程的名称,线程组,优先级,守护进程状态等。如果ThreadCactory在通过从new Thread返回null询问时未能创建线程,则执行程序将继续,但可能无法执行任何任务。也可以通过实现ThreadFactory自定义线程工厂。

线程池核心类 ThreadPoolExecutor 为自定义线程池,提供了多个构造方法来创建线程池。我们看一下完成参数的线程池构造函数,可以看到ThreadFactory这个参数:

public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler);

Executors工具类提供了六类可供创建的Executor执行器实例,也都有包含 ThreadFactory 参数的构造器,如下:

/**
 * 创建一个具有固定线程数的Executor.
 * 在需要时使用提供的 ThreadFactory 创建新线程.
 */
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(), threadFactory);

}
 
/**
 * 创建一个使用单个 worker 线程的 Executor.
 * 在需要时使用提供的 ThreadFactory 创建新线程.
 */
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(), threadFactory));
}

/**
 * 创建一个具有固定线程数的 可调度Executor.
 * 它可安排任务在指定延迟后或周期性地执行.
 * 在需要时使用提供的 ThreadFactory 创建新线程.
 */
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

2、线程/线程池名称问题

在使用多线程或线程池时,如果我们没有一个好的命名的话,出问题的时候就会难以定位。

多线程代码如下:

public class Demo {
    public static void main( String[] args ) throws IOException {
        new Thread(()->{
            System.out.println("保存用户信息..........");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new NullPointerException();
        }).start();
        System.in.read();
    }
}

上面代码是启动一个线程来保存用户信息,然后抛出异常!报错信息如下:

image-1670816596418

从运行错误可以分析,Thread-0 抛出了空指针,那么单从这个日志根本无法判断用户模块线程抛出的异常。我们先分析一下Thread-0是怎么来的。

我们先看下创建线程的代码:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
    
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

从上面代码可知,如果我们没有指定线程名称,内部会自动为我们创建线程名称"Thread-"+nextThreadNum()作为线程的默认名。

如果一个系统中有多个业务模块,如用户模块、订单模块、购物车模块等都使用自己的线程池,而没有指定名称,抛出的异常除非与业务内容有关,否则,根本无法判断是哪一个模块出了问题。

线程池代码如下:

同理创建线程池时也需要指定名称,如下没有指定名称的代码:

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

public class Demo {
    public static void main(String[] args) throws IOException {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        // 该方法实际调用的是ThreadPoolExecutor的创建方式
        threadPool.execute(new Thread(() -> {
            System.out.println("保存用户信息..........");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new NullPointerException();
        }));
        System.in.read();
    }
}

public class Demo2 {

    public static void main( String[] args ) throws IOException {
        ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
        executorOne.execute(()->{
            System.out.println("保存用户信息");
            throw new NullPointerException();
        });
        System.in.read();
    }
}

和多线程代码异常一样!报错信息如下:

image-1670817080295

可以看到线程名称为 pool-1-thread-1, 多了前面的pool-1为线程池工厂名称和编号,pool为系统默认的,如果有多个线程池的时候打印日志都为pool,是无法区分线程池的。

我们看下ThreadPoolExecutor源码:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

Executors.defaultThreadFactory()源码:

 public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }

DefaultThreadFactory源码:

DefaultThreadFactory() {
      SecurityManager s = System.getSecurityManager();
      group = (s != null) ? s.getThreadGroup() :
                            Thread.currentThread().getThreadGroup();
      namePrefix = "pool-" +
                    poolNumber.getAndIncrement() +
                   "-thread-";
  }
    ```
    ```
 private static final AtomicInteger poolNumber = new AtomicInteger(1);

到这里我们就可以看出来,默认的线程工厂使用的名称前缀为“pool-”poolNumber.getAndIncrement() + “-thread-”, 所以我们可以自定义线程工厂的实现方式,来指定线程池名称。

3、多线程名称

3.1 使用Thread+Runnable接口形式

如果是使用实现Runnable接口,然后使用Thread构造器来直接创建线程时,有两种方式设置线程名称:

  1. 在调用Thread的构造器时,传入第二个参数即可,构造器定义如下:
Thread Thread(Runnable target, String threadName)

示例如下:

public class Demo {
    public static void main( String[] args ) throws IOException {
        new Thread(()->{
            System.out.println("保存用户信息..........");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new NullPointerException();
        }, "my-thread-name-1").start();
        System.in.read();
    }
}
  1. 调用Thread对象的setName方法,设置线程名称即可;
public class Demo {
    public static void main( String[] args ) throws IOException {
        Thread thread = new Thread(()->{
            System.out.println("保存用户信息..........");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new NullPointerException();
        });
        
        thread.setName("my-thread-name-2");
        thread.start();
        System.in.read();
    }
}

3.2 继承Thread类的形式

如果是继承Thread类,那么可以在子类中调用Thread仅接受一个字符串作为线程名称的构造器:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * 线程池名称
 *
 * @Author kongzi
 * @Date 2022/12/13 09:06
 * @Version 1.0
 */
public class ThreadTest {
    private static Logger logger = LoggerFactory.getLogger(ThreadTest.class);

    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();
            logger.info("threadName:{}, threadGroupName:{}, threadId:{}", threadName, threadGroupName, threadId);
        }
    }

    public static void main(String[] args) throws IOException {
        MyThread t1 = new MyThread("my-extends-thread-name-1");
        t1.start();

        MyThread t2 = new MyThread("my-extends-thread-name-2");
        t2.setName("changed-thread-name"); // 手动修改线程名称
        t2.start();
    }
    
}

执行结果:

09:16:40.103 [changed-thread-name] INFO com.kz.example.ThreadTest - threadName:changed-thread-name, threadGroupName:main, threadId:13
09:16:40.103 [my-extends-thread-name-1] INFO com.kz.example.ThreadTest - threadName:my-extends-thread-name-1, threadGroupName:main, threadId:12

可以看到,第一个线程t1打印了名称为my-extends-thread-name-1,第二个线程t2创建构造函数的时候用的名称 my-extends-thread-name-2,但又通过setName方法重设了线程名称,所以打印出来的名称是重设后的 changed-thread-name。

3.3 设置线程组的名称

线程组名称需要在创建线程组的时候进行指定,然后使用线程组的时候将线程组作为Thread类的构造器参数传入即可,示例代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * 线程池组名称
 *
 * @Author kongzi
 * @Date 2022/12/13 09:06
 * @Version 1.0
 */
public class ThreadGroupTest {
    private static Logger logger = LoggerFactory.getLogger(ThreadGroupTest.class);

    public static 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();
            logger.info("threadGroupName:{}, threadName:{}, threadId:{}", threadGroupName, threadName, threadId);
        };

        Thread t1 = new Thread(threadGroup, runnable);
        t1.start();
        // 输出 threadGroupName:my-thread-group-name, threadName:Thread-0, threadId:12

        // 第三个参数是线程名称
        Thread t2 = new Thread(threadGroup, runnable, "my-thread-name");
        t2.start();
        // 输出 threadGroupName:my-thread-group-name, threadName:my-thread-name, threadId:13
    }


    public static void main(String[] args) throws IOException {
        defineThreadGroupName();
    }
}

4、线程池名称

4.1 自定义线程工厂方式

自定义线程工厂,重写生产线程工厂的命名方法。

package com.kz.example.thread;

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

/**
 * 自定义ThreadFactory,重写生产线程工厂的命名方法
 *
 * @Author kongzi
 * @Date 2022/12/12 10:26
 * @Version 1.0
 */
public class SelfNamedThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private String namePrefix;
    private final ThreadGroup group;

    public SelfNamedThreadFactory(String name) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        if (null == name || name.isEmpty()) {
            name = "pool";
        }
        this.namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
    }

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

测试代码如下:

package com.kz.example.thread;

import java.util.concurrent.*;

/**
 * ThreadPoolExecutor 测试类
 *
 * @Author kongzi
 * @Date 2022/12/12 10:17
 * @Version 1.0
 */
public class ThreadPoolExecutorTest {
    public static void main(String[] args) {

        ThreadFactory threadFactory = new SelfNamedThreadFactory("自定义业务");

        ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 10000,
                TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), threadFactory);

        //创建实现了Runnable接口的类,如Thread
        MyThreads t1 = new MyThreads();
        MyThreads t2 = new MyThreads();
        MyThreads t3 = new MyThreads();
        MyThreads t4 = new MyThreads();
        //将线程放入池中执行
        threadPool.execute(t1);
        threadPool.execute(t2);
        threadPool.execute(t3);
        threadPool.execute(t4);
        //关闭线程池
        threadPool.shutdown();
    }
}

class MyThreads extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行...");
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

自定义业务-1-thread-3正在执行...
自定义业务-1-thread-4正在执行...
自定义业务-1-thread-1正在执行...
自定义业务-1-thread-2正在执行...

可以看到,线程池生产的线程名称已经根据线程池的命名做了区分,这样我们有多个线程池的时候,可以很好的区分开他们的日志记录。

4.2 其他方式

除了自定义线程工厂方式外,我们还可以使用一些现有的工具类。

4.2.1 使用lucene-core

如果是maven项目引入依赖如下:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>7.7.0</version>
    <scope>compile</scope>
</dependency>

非maven项目可以导入jar包:lucene-core-7.7.0.jar。

使用方式:

package com.kz.example.thread;

import org.apache.lucene.util.NamedThreadFactory;

import java.io.IOException;
import java.util.concurrent.*;

/**
 * 线程池名称
 *
 * @Author kongzi
 * @Date 2022/12/12 11:46
 * @Version 1.0
 */
public class FixedThreadPoolDemo {
    public static void main(String[] args) throws IOException {
        ThreadFactory threadFactory = new NamedThreadFactory("自定义业务");
        ExecutorService executorService = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new LinkedBlockingDeque<>(), threadFactory);
        executorService.execute(new Thread(() -> {
            System.out.println("保存用户信息..........");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new NullPointerException();
        }));
        System.in.read();
    }
}

执行结果:

保存用户信息..........
Exception in thread "自定义业务-1-thread-1" java.lang.NullPointerException
	at com.kz.example.thread.FixedThreadPoolDemo.lambda$main$0(FixedThreadPoolDemo.java:27)
	at com.kz.example.thread.FixedThreadPoolDemo$$Lambda$1/1921595561.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

4.2.2 使用CustomizableThreadFactory

spring为我们提供了自定义线程池的工具类: CustomizableThreadFactory,可以直接使用:

private ThreadFactory springThreadFactory = new CustomizableThreadFactory("springThread-pool-");
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
								10, 
								30, 
								5, 
								TimeUnit.MINUTES, 
								new ArrayBlockingQueue<Runnable>(1000),
								springThreadFactory ); //给线程池中的线程自定义名称

该工具类无法区分线程池的线程序号。

4.2.3 ThreadFactoryBuilder

Google guava 工具类 提供的 ThreadFactoryBuilder ,使用链式方法创建。

private ThreadFactory guavaThreadFactory = new ThreadFactoryBuilder().setNameFormat("retryClient-pool-").build();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
								10, 
								30, 
								5, 
								TimeUnit.MINUTES, 
								new ArrayBlockingQueue<Runnable>(1000),
								guavaThreadFactory ); //给线程池中的线程自定义名称
                                

4.2.4 BasicThreadFactory

pache commons-lang3 提供的 BasicThreadFactory

private ThreadFactory basicThreadFactory = new BasicThreadFactory.Builder().namingPattern("basicThreadFactory-").build();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
								10, 
								30, 
								5, 
								TimeUnit.MINUTES, 
								new ArrayBlockingQueue<Runnable>(1000),
								basicThreadFactory ); //给线程池中的线程自定义名称

5、总结

创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。可以自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给前缀。

image-1670819510408

0

评论区