在日常开发项目中,我们一般都会创建多个线程池用来资源隔离,如果没有给线程池指定名称的话,万一线程抛出异常了,日志记录将无法定位到底是哪个线程池抛出的异常;所以为了方便排查,给线程池的线程自定义命名,主要方法就是重写生产线程工厂的命名方法。
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();
}
}
上面代码是启动一个线程来保存用户信息,然后抛出异常!报错信息如下:
从运行错误可以分析,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();
}
}
和多线程代码异常一样!报错信息如下:
可以看到线程名称为 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构造器来直接创建线程时,有两种方式设置线程名称:
- 在调用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();
}
}
- 调用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、总结
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。可以自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给前缀。
评论区