1、使用线程池的好处
线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、程序计数器、本地方法栈等线程私有的内存空间。在线程销毁时需要回收这些资源。频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新线程等待或者友好地拒绝服务?这都是线程自身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。线程池的作用包括:
- 利用线程池管理并复用线程、控制最大并发数。
- 实现任务线程队列缓存策略和拒绝机制。
- 实现某些与实践相关的功能,如定时执行、周期执行等。
- 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响。
2、ThreadPoolExecutor
了解线程池的基本作用后,我们学习一下线程池是如何创建线程的。首先从ThreadPoolExecutor构造方法讲起,学习如何自定义ThreadFactory和RejectedExecutionHandler,并编写一个最简单的线程池示例。然后通过分析ThreadPoolExecutor的execute和addWorker两个核心方法,学习如何把任务线程加入到线程池中运行。
ThreadPoolExecutor构造方法如下:
public ThreadPoolExecutor(
int corePoolSize, //参数1
int maximumPoolSize, //参数2
long keepAliveTime, //参数3
TimeUnit unit, //参数4
BlockingQueue<Runnable> workQueue, //参数5
ThreadFactory threadFactory, //参数6
RejectedExecutionHandler handler) { //参数7
if (corePoolSize < 0 ||
//maximumPoolSize必须大于或等于1,也要大于或等于corePoolSize(第一处)
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
//(第二处)
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- 参数1,corePoolSize:表示常驻核心线程数。如果等于0,则任务执行完成以后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
- 参数2,maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。从上方示例代码中的第一处来看,必须大于或等于1.如果待执行的线程数大于此值,需要借助第五个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小线程池。
- 参数3,keepAliveTime:表示线程池中线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,直到只剩下corePoolSize个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于corePoolSize时,keepAliveTime才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量设置为true时,核心线程超时后也会被回收。
- 参数4,TimeUnit:表示时间单位。KeepAliveTime的时间单位通常是TimeUnit.SECONDS。
- 参数5,workQueue:表示缓存队列。当请求的线程数大于corePoolSize时,线程进入BlockingQueue阻塞队列(请注意,是当corePoolSize不够用时,将任务加入缓存队列,当缓存队列也容纳不下任务时,再开辟新的线程来处理任务,直到线程数到达maximumPoolSize)。后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取,是一个生产消费模型队列。
- 参数6,threadFactory表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
- 参数7,handler表示执行拒绝策略的对象。当第五个参数workQueue的任务缓存区到达上限后,并且活动线程数等于maximumPoolSize的时候,线程池通过该策略处理请求,这是一种简单的限流保护。友好地拒绝策略可以是如下三种:保存到数据库进行削峰填谷,在空闲时再提取出来执行、转向某个提示页面、打印日志。
从代码中的第二处来看,队列、线程工厂、拒绝处理服务都必须有示例对象,但在实际编程中,很少有程序员对这三者进行实例化,而是通过Executors这个线程池静态工厂提供默认实现,那么Executors与ThreadPoolExecutor是什么关系呢?(从源码上看,ThreadPoolExecutor是Executors的底层实现)线程池相关类图如下所示:
ExecutorService接口继承了Executor接口,定义了管理线程任务的方法。ExecutorService的抽象类AbstractExecutorService提供了submit()、invokeAll()等部分方法的实现。但是核心方法Executor.execute()并没有在这里实现。因为所有的任务都在这个方法里执行,不同实现会带来不同的执行策略。通过Executor的静态工厂方法可以创建三个线程池的包装对象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。
Executors(Java提供用来创建线程池的类,在JUC包下)的核心方法有五个(这五个方法每个都有多种重载方法,我只选取了一种):
- Executor.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU数量设置为默认的并行度。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
- Executors.newCachedThreadPool:maximumPoolSize最大可以至Integer.MAX_VALUE,是高度可伸缩的线程池,如果达到这个上限,相信没有任何服务器能够继续工作,肯定会抛出OOM异常。keepAliveTime默认60秒,工作线程处于空闲状态,则回收工作线程。如果任务数增加,再次创建出新线程处理任务。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
- Executors.newScheduledThreadPool:线程数最大值Integer.MAX_VALUE,与上述相同,存在OOM风险。它是ScheduledExecutorService接口家族的实现类,支持定时及周期性任务执行。相比Timer,ScheduledExecutorService更安全,功能更加强大,与newCachedThreadPool的区别是不回收工作线程。
public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); }
- Executors.newSingleThreadExecutor:创建一个单线程的线程池,相当於单线程串行执行所有任务,保证按任务的提交顺序依次执行。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
- Executors.newFixedThreadPool:输入的参数即是固定线程数,即是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime等于0:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } //这里输入的队列没有指明长度,看一下LinkedBlockingQueue的构造方法 public LinkedBlockingQueue(){ this(Integer.MAX_VALUE); } //使用这样的无界队列,如果瞬间请求量很大的话,会有OOM风险
除了newWorkStealingPool外,其他四个创建方式都存在资源耗尽的风险。
Executors中默认的线程工厂(threadFactory)和拒绝策略(handler)过于简单,通常对用户来说都不太友好,线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号。拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转,以下为简单的ThreadFactory示例:
package Test;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class UserThreadFactory implements ThreadFactory {
//name prefix译为名称前缀
private final String namePrefix;
//AtomicInteger是一个原子操作类,能够保证操作的原子性
private final AtomicInteger nextId = new AtomicInteger();
//定义线程组名称whatFeatureOfGroup,在使用jstack来排查线程问题时,非常有帮助
UserThreadFactory(String whatFeatureOfGroup){
this.namePrefix = "UserThreadFactory's" + whatFeatureOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName());
return thread;
}
}
//任务执行体
class Task implements Runnable{
private final AtomicLong count = new AtomicLong(0L);
@Override
public void run() {
System.out.println("running_" + count.getAndIncrement());
}
}
这个示例包括线程工厂和任务执行体的定义,通过newThread方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯。
简单地实现一下RejectedExecutionHandler,实现了接口的rejectedExecution方法,打印出当前线程池状态:
public class UserRejectHandler implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor){
System.out.println("task rejected." + executor.toString());
}
}
当一个任务Task欲通过threadPoolexecutor.execute(Runnable)添加到线程中时,会出现以下几种情况:
- 此时线程池中的线程数小于corePoolSize(可以通过设置参数让核心线程也空闲超时销毁)时,即使线程池中的线程都处于空闲状态,那么也要创建一个新的线程来执行这个任务Task;
- 此时线程池中线程数量等于corePoolSize,但是缓冲队列workQueue未满,则将任务加入缓存队列等待处理;
- 此时线程池中线程数量大于corePoolSize,缓冲队列workQueue已满,但是线程数量小于maximumPoolSize时,则新建一个线程来执行此任务Task;
- 此时线程池中线程数量大于corePoolSize,缓冲队列workQueue已满,且线程数量等于maximumPoolSize时,那么通过指定的handler来对该任务Task执行拒绝策略。
handler有四个选择,在ThreadPoolExecutor中提供了四个公开的静态内部类:
- AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
public static class AbortPolicy implements RejectedExecutionHandler { /** * Creates an {@code AbortPolicy}. */ public AbortPolicy() { } /** * Always throws RejectedExecutionException. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task * @throws RejectedExecutionException always. */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
- DiscardPolicy:丢弃任务,但不抛出异常,这是不推荐的做法。
public static class DiscardPolicy implements RejectedExecutionHandler { /** * Creates a {@code DiscardPolicy}. */ public DiscardPolicy() { } /** * Does nothing, which has the effect of discarding task r. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } }
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。
public static class DiscardOldestPolicy implements RejectedExecutionHandler { /** * Creates a {@code DiscardOldestPolicy} for the given executor. */ public DiscardOldestPolicy() { } /** * Obtains and ignores the next task that the executor * would otherwise execute, if one is immediately available, * and then retries execution of task r, unless the executor * is shut down, in which case task r is instead discarded. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } }
- CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
public static class CallerRunsPolicy implements RejectedExecutionHandler { /** * Creates a {@code CallerRunsPolicy}. */ public CallerRunsPolicy() { } /** * Executes task r in the caller's thread, unless the executor * has been shut down, in which case the task is discarded. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } }
使用方式如下代码所示:
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
根据之前实现的线程工厂和拒绝策略,一个简单线程池的相关示例代码实现如下:
package Test;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class UserThreadPool {
public static void main(String[] args) {
//缓存队列设置固定长度为2,为了快速触发rejectHandler
BlockingQueue queue = new LinkedBlockingQueue(2);
//假设外部任务线程的来源有机房1和机房2混合调用
UserThreadFactory f1 = new UserThreadFactory("第1机房");
UserThreadFactory f2 = new UserThreadFactory("第2机房");
UserRejectHandler handler = new UserRejectHandler();
//核心线程为1,最大线程为2,为了保证触发rejectHandler
ThreadPoolExecutor threadPoolFirst = new ThreadPoolExecutor(
1, 2, 60, TimeUnit.SECONDS, queue, f1, handler);
//利用第二个线程工厂实例创建第二个线程池
ThreadPoolExecutor threadPoolSecond = new ThreadPoolExecutor(
1, 2, 60, TimeUnit.SECONDS, queue, f2, handler);
//创建400个任务线程
Runnable task = new Task();
for (int i = 0; i < 200; i++){
threadPoolFirst.execute(task);
threadPoolSecond.execute(task);
}
}
}
运行结果如下:
From UserThreadFactory's 第1机房 -Worker-1
From UserThreadFactory's 第2机房 -Worker-1
From UserThreadFactory's 第1机房 -Worker-2
From UserThreadFactory's 第2机房 -Worker-2
running_2
running_3
running_4
running_5
running_0
running_1
your task is reject. java.util.concurrent.ThreadPoolExecutor@1396fbe[Running, pool size = 2, active thread = 2,
queued tasks =2,completed task =1]
当任务被拒绝时,拒绝策略会打印出当前线程池的大小已经达到了maximumPoolSize=2,且队列已满,完成的任务数提示已经有一个(最后一行)。
3、ThreadPoolExecutor部分源码解析(execute()和addWorker())
在ThreadPoolExecutor的属性定义中频繁地用位移运算来表示线程池的状态,位移运算是改变当前值的一种高效手段,包括左移和右移,下面从属性定义开始阅读源码:
//Integer共有32位,最右边29位表示工作线程数,最左边3位表示线程池状态
//注:简单地说,3个二进制位可以表示从0到7的八个不同数值(第1处)
private static final int COUNT_BITS = Integer.SIZE - 3;
//000-11111111111111111111111111111,类似于子网掩码,用于位的与运算,
//得到左边3位,还是右边29位
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//用左边3位,实现5种线程池状态。
//111-00000000000000000000000000000,十进制:-536,870,912。
//此状态表示线程池能够接受新任务
private static final int RUNNING = -1 << COUNT_BITS;
//000-00000000000000000000000000000,十进制:0。
//此状态表示不再接受新任务,但可以继续执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
//001-00000000000000000000000000000,十进制:536,870,912。
//此状态表示全面拒绝,并中断正在处理的任务
private static final int STOP = 1 << COUNT_BITS;
//010-00000000000000000000000000000,十进制:1,073,741,824。
//此状态表示所有任务已经被终止
private static final int TIDYING = 2 << COUNT_BITS;
//011-00000000000000000000000000000,十进制:1,610,612,736。
//此状态表示已清理完现场
private static final int TERMINATED = 3 << COUNT_BITS;
//与运算,比如001-00000000000000000000000000011,表示67个工作线程,
//掩码取反:111-00000000000000000000000000000,即得到左边3位001,
//表示线程池当前处于STOP状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//同理掩码000-111111111111111111111111111111,得到右边29位,即工作线程数
private static int workerCountOf(int c) { return c & CAPACITY; }
//把左边3位与右边29位按或运算,合并成一个值
private static int ctlOf(int rs, int wc) { return rs | wc; }
第1处说明:线程池的状态用高3位表示,其中包括了符号位。五种状态的十进制值按从小到大依次排序为:
RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED。
这样设计的好处是可以通过比较值的大小来确定线程池的状态,例如程序中经常出现isRunning的判断:
private static boolean isRunning(int c){
return c < SHUTDOWN;
}
Executor接口有且只有一个方法execute,通过参数传入待执行线程的对象,下面分析ThreadPoolExecutor关于execute方法的实现:
public void execute(Runnable command) {
//JDK中,command的解释为:@param command the runnable task
if (command == null)
throw new NullPointerException();
//返回包含线程数及线程池状态的Integer类型数值
int c = ctl.get();
//如果工作线程数小于核心线程数,则创建线程任务并执行
if (workerCountOf(c) < corePoolSize) {
//addWorker是另一个极为重要的方法,见下一段源码解析(第1处)
if (addWorker(command, true))
return;
//如果创建失败,防止外部已经在线程池中加入新的任务,重新获取一下
c = ctl.get();
}
//只有当线程池处于RUNNING状态,才执行后半句:置入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//如果线程池不是RUNNING状态,则将刚加入队列的任务移除
if (! isRunning(recheck) && remove(command))
reject(command);
//如果之前的线程已被消费完,新建一个线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//核心池和队列都已满,尝试创建一个新线程
else if (!addWorker(command, false))
//如果addWorker返回是false,即创建失败,则唤醒拒绝策略(第2处)
reject(command);
}
- 第1处:execute方法在不同的阶段有三次addWorker的尝试动作。
- 第2处:发生拒绝的理由有两个:(1)线程池状态为非RUNNING状态;(2)等待队列已满。
下面分析addWorker方法的源码:
/**
*根据当前线程池状态,检查是否可以添加新的任务线程,如果可以则创建并启动任务
* 如果一切正常且返回true。返回false的可能性如下:
* 1、线程池没有处于RUNNING状态;
* 2、线程工厂创建新的任务线程失败
*
* firstTask:外部启动线程池时所需要构造的第一个线程,它是线程的母体
* core:新增工作线程时的判断指标,解释如下
* true 表示新增工作线程时,需要判断当前RUNNING状态的线程是否少于corePoolSize
* false 表示新增工作线程时,需要判断当前RUNNING状态的线程是否少于maximumPoolSize
*/
private boolean addWorker(Runnable firstTask, boolean core) {
//不需要任务预定义的语法标签,响应下文的continue retry,快速推出多层嵌套循环(第1处)
retry:
for (;;) {
//获取线程池状态及线程数
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
//如果是STOP及以上的状态,或者firstTask初始线程不为空,或者队列为空都会直接返回创建失败(第2处)
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//如果超过最大允许线程数则不能再添加新的线程
//最大线程数不能超过2^29,否则影响左边3位的线程池状态值
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//将当前活动线程数+1(第3处)
if (compareAndIncrementWorkerCount(c))
break retry;
//线程池状态和工作线程数是可变化的,需要经常提取这个最新值
c = ctl.get(); // Re-read ctl
//如果已经关闭,则再次从retry标签处进入,在第2处再做判断(第4处)
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
//如果线程还是处于RUNNING状态,那就在说明仅仅是第3处失败
//继续循环执行(第5处)
}
}
//开始创建工作线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//利用Worker构造方法中的线程池工厂创建线程,并封装成工作线程Worker对象
w = new Worker(firstTask);
//注意这是Worker中的属性对象thread(第6处)
final Thread t = w.thread;
if (t != null) {
//在进行ThreadPoolExecutor的敏感操作时都需要持有主锁
//避免在添加和启动线程时被干扰
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
//当线程池状态为RUNNING或SHUTDOWN
//且firstTask初始线程为空时
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
//整个线程池在运行期间的最大并发任务个数
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//终于看到亲切的start()方法
//注意,并非线程池的execute的command参数指向的线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
//线程启动失败,把刚才第3处加上的工作线程计数再减回去
addWorkerFailed(w);
}
return workerStarted;
}
- 第1处:配合循环语句出现的label,类似于goto作用。label定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错。目的是实现多重循环时能够快速推出到任何一层。这种做法的出发点似乎非常贴心,但是在大型软件项目中,滥用标签行跳转的后果将是灾难性的。示例代码中,在retry下方有两个无限循环,在workerCount加1成功后,直接推出两层循环。
- 第3处:与第1处的标签呼应,AtomicInteger对象的加1操作时原子性的。break retry表示直接跳出与retry相邻的这个循环体。
- 第4处:此continue跳转至标签处,继续执行循环。如果条件为假,则说明线程池还处于运行状态,即继续在for(;;)循环内执行。
- 第5处:compareAndIncrementWorkerCount方法执行失败的概率非常低。即使失败,再次执行时成功的概率也是极高的,类似于自旋锁原理。这里的处理逻辑是先加1,创建失败减1,这是轻量级处理并发创建线程的方式。如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大。
- 第6处:Worker对象时工作线程的核心类实现,部分源码如下:
//它实现Runnable接口,并把本对象作为参数输入给run()方法中的runWorker(this), //所以内部属性线程thread在start的时候,即会调用runWorker方法 private final class Worker extends AbstarctQueueSynchronizer implements Runnable{ Worker(Runnable firstTask){ //它是AbstractQueueSynchronizer的方法 //在runWorker方法执行之前禁止线程被中断 setState(-1); this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } //当thread被start()之后,执行runWorkder的方法 public void run(){ runWorker(this); } }
码出高效中作者的建议:使用线程池要注意以下几点:
- 合理设置各类参数,应根据实际业务场景来说设置合理地工作线程数。
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
阿里的Java开发手册中明确提出:线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险(Executors不能指定所有的参数,默认参数可能会造成资源浪费)。