声明:本篇文章转载自并发编程网 – ifeve.com作者加多大佬的文章,目的是更好的阐释《阿里巴巴Java开发规约》第一章中的并发处理篇关于创建线程或线程池是为什么要指定有意义的线程名称相关问题。由于老四在本篇文章中优化改编了某些阐述语言,并修改了一些源码排版,再加上未找到加多的联系方式,所以如果发现本篇文章涉及到侵权问题,请及时联系老四,以便立刻删除。
本文的主要目的是解释《阿里巴巴Java开发规约第一章-并发处理篇》中第二条”创建线程或线程池时请指定有意义的线程名称,方便出错时回溯”的相关问题,一下是正文:
我们在日常开发中,当一个应用中需要创建多个线程或者线程池时候最好给每个线程或者线程池根据业务类型设置具体的名字,以便于在出现问题时候方便进行定位出错地址,下面就通过实例来说明不设置时候为何难以定位问题,以及如何进行线程或者线程池名称的设置。
1.为什么创建线程需要带上自定义的线程名称
下面通过简单的代码来说明不指定线程名称为何难定位问题,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package com.glorze.thread; /** * 创建线程不自定义名称 * @ClassName ThreadWithoutName * @author: glorze.com * @since: 2018年8月29日 下午11:58:16 */ public class ThreadWithoutName { public static void main(String[] args) { //订单模块 Thread threadOne = new Thread(new Runnable() { @Override public void run() { System.out.println("保存订单的线程"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } throw new NullPointerException(); } }); //发货模块 Thread threadTwo = new Thread(new Runnable() { @Override public void run() { System.out.println("保存收获地址的线程"); } }); threadOne.start(); threadTwo.start(); } } |
以上代码分别创建了两个线程,不过代码执行的过程中可能看到如下所示的异常:
从异常日志中我们可以看到Thred-0抛出了NPE(NullPointerException, 空调格指针异常),那么单看这个日志根本无法判断是订单模块的线程抛出的异常,首先我们分析下这个Thread-0是怎么来的,这要看下创建线程时候的代码(来源于jdk: java.lang.Thread.class):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/** * 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. * * 大意就是该构造方法是用来创建一个新的线程,在没有指定线程名称的情况下, * 自动生成的名称具有形如"Thread-"的形式。 * * @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. * 调用target,如果target为空,则不会做任何事情 */ public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } /** * Initializes a Thread with the current AccessControlContext. * * 使用当前的AccessControlContext初始化一个线程。 * * @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean) */ private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } /* For autonumbering anonymous threads. */ /* 用于自动编写匿名线程 */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; } |
从Thread.class源码类中,我们可以看出如果调用了没有指定线程名字的方法创建了线程,内部会使用”Thread-“+nextThreadNum()作为线程的默认名字。可知threadInitNumber是static变量,nextThreadNum是static方法,所以线程的编号是全应用唯一的并且是递增的,另外这里由于涉及到了多线程递增threadInitNumber也就是执行读取-递增-写入操作,而这个是线程是不安全的,所以又使用了方法级别的synchronized进行了同步。关于Java中static的基本使用可以参考一下老四的这篇《Java面向对象之类成员浅析》文章。
所以当一个系统中有多个业务模块并且每个模块中都有自己要跑的线程,如果遇到问题除非是抛出与业务相关的异常,否则要是都抛出类似上面的NPE异常,根本没法判断是哪一个模块出现了问题,现在将以上代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package com.glorze.thread; /** * 创建带有线程名称的线程类 * @ClassName ThreadWithName * @author: glorze.com * @since: 2018年8月30日 下午1:28:25 */ public class ThreadWithName { 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() { @Override public void run() { System.out.println("保存订单的线程"); throw new NullPointerException(); } }, THREAD_SAVE_ORDER); // 发货模块 Thread threadTwo = new Thread(new Runnable() { @Override public void run() { System.out.println("保存收货地址的线程"); } }, THREAD_SAVE_ADDR); threadOne.start(); threadTwo.start(); } } |
再次运行,异常信息如下图所示,在创建线程的时候给线程指定了一个与具体业务模块相关的名字,从运行结果就可以定位到是保存订单模块抛出了NPE异常,一下子就可以定位到问题。
2.为什么创建线程池时候也需要指定线程池的名称?
同理下面通过简单的代码来说明不指定线程池名称为何难定位问题,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.glorze.thread import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 创建线程池时不自定义名称 * @ClassName ThreadPoolWithoutName * @author: glorze.com * @since: 2018年8月30日 下午1:36:18 */ public class ThreadPoolWithoutName { 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() { @Override public void run() { System.out.println("接受用户链接线程"); throw new NullPointerException(); } }); // 具体处理用户请求模块 executorTwo.execute(new Runnable() { @Override public void run() { System.out.println("具体处理业务请求线程"); } }); executorOne.shutdown(); executorTwo.shutdown(); } } |
运行代码输出结果如下:
同理我们并不知道是那个模块的线程池抛出了这个异常,那么我们看下这个pool-1-thread-1是如何来的。其实是使用了线程池默认的ThreadFactory,翻看线程池创建的源码如下(源码来自jdk: java.util.concurrent.ThreadPoolExecutor.class & java.util.concurrent.Executor.class & java.util.concurrent.Executor$DefaultThreadFactory):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
/** * 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); } /** * 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; } } |
如上代码DefaultThreadFactory的实现可知:
- poolNumber是static的原子变量用来记录当前线程池的编号是应用级别的,所有线程池公用一个,比如创建第一个线程池时候线程池编号为1,创建第二个线程池时候线程池的编号为2,这里pool-1-thread-1里面的pool-1中的1就是这个值。
- threadNumber是线程池级别的,每个线程池有一个该变量用来记录该线程池中线程的编号,这里pool-1-thread-1里面的thread-1中的1就是这个值。
- namePrefix是线程池中线程的前缀,默认固定为pool。
- 具体创建线程,可知线程的名称使用namePrefix + threadNumber.getAndIncrement()拼接的。
从上知道我们只需对实现ThreadFactory并对DefaultThreadFactory的代码中namePrefix的初始化做手脚,当需要创建线程池是传入与业务相关的namePrefix名称就可以了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package com.glorze.thread; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * 创建线程池时自定义名称 * @ClassName ThreadPoolWithoutName * @author: 高老四 * @since: 2018年8月30日 下午1:36:18 */ public class ThreadPoolWithName { /** * 命名线程工厂 * @ClassName NamedThreadFactory * @author: 高老四 * @since: 2018年9月3日 下午12:30:21 */ static class NamedThreadFactory implements ThreadFactory { private static final AtomicInteger POOL_NUMBER = 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 + "-" + POOL_NUMBER.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; } } 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")); public static void main(String[] args) { //接受用户链接模块 executorOne.execute(new Runnable() { @Override public void run() { System.out.println("接受用户链接线程"); throw new NullPointerException(); } }); //具体处理用户请求模块 executorTwo.execute(new Runnable() { @Override public void run() { System.out.println("具体处理业务请求线程"); } }); executorOne.shutdown(); executorTwo.shutdown(); } } |
然后运行执行结果如下:
从ASYN-ACCEPT-POOL-1-thread-1就可以知道是接受链接线程池抛出的异常。
本文转载自并发编程网-ifeve.com,原文链接: 创建线程以及线程池时候要指定与业务相关的名字,以便于追溯问题。
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额,老四这里抱拳了。赞助时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的赞赏钱财也会被用于小站的服务器运维上面,再次抱拳。