Java多线程之线程池
一、线程池的自我介绍
1. 线程池的重要性(为什么使用线程池)
线程池可以应对突然大爆发量的访问,通过有限个固定线程为大量的操作服务,减少创建和销毁线程所需的时间。
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2. 线程池的使用场景
(1) ***高并发、任务执行时间短***的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换。但是当处理线程数设置极大的时候和非线程池模型几乎没有差别
(2) ***并发不高、任务执行时间长***的业务要区分来看:
-
假如是业务时间长集中在IO操作上,也就是***IO密集型的任务***,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务。
-
假如是业务时间长集中在计算操作上,也就是***计算密集型任务***,这个就没办法了,和一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换。
(3) 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考第二小步。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
二、创建和停止线程
1. 线程池构造函数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize |
int |
核心线程数 |
maxPoolSize |
int |
最大线程数 |
keepAliveTime |
long |
保持存活的时间 |
workQueue |
BlockingQueue |
任务存储队列 |
threadFactory |
ThreadFactory |
当线程池需要使用新的线程是,会使用threadFactory 来创建新的线程 |
Handler |
RejectedExecutionHandler |
由于线程池无法接受你锁提交的任务的拒绝策略 |
详细介绍:
corePoolSize
:线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。如果线程池中的线程少于此数目,则在执行任务时创建。maximumPoolSize
和maxPoolSize
:池允许最大的线程数,当线程数量达到corePoolSize
,且workQueue
队列塞满任务了之后,继续创建线程。keepAliveTime
:超过corePoolSize
之后的“临时线程”的存活时间。unit
:keepAliveTime
的单位。workQueue
:当前线程数超过corePoolSize
时,新的任务会处在等待状态,并存在workQueue
中,BlockingQueue
是一个先进先出的阻塞式队列实现,底层实现会涉及Java并发的AQS
机制。threadFactory
:创建线程的工厂类,通常我们会自顶一个threadFactory
设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位。handler
:线程池执行拒绝策略,当线数量达到maximumPoolSize
大小,并且workQueue
也已经塞满了任务的情况下,线程池会调用handler
拒绝策略来处理请求。
2. 线程池的创建
线程添加规则:
举个例子:线程池核心池大小为5, 最大池大小为10,队列为100。
那么线程添加的规则是:线程请求最多会创建5个,然后任务将会被添加到队列中,知道达到100, 当队列已满,将会创建新的线程,知道线程最大10,如果再来任务,就拒绝。
线程池应该手动创建,这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。接下来看一下jdk
为我们提供的几种线程池:
Executors.newFixedThreadPool
:定长线程池
-
简介:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
-
适用: 执行长期的任务,性能好很多。
-
构造函数:
corePoolSize
和maximumPoolSize
:nThreads
keepAliveTime
:0L
(不限时:存活时间无限)unit
:TimeUnit.MILLISECONDS
workQueue
:LinkedBlockingQueue
该无界队列容量是Integer.MAX_VALUE
,吞吐量通常要高于ArrayBlockingQueue
。由于没有上限,当任务越来越多,并且无法及时处理完毕,也就是请求堆积越来越多,会容易造成占用大量的内存,会导致OOM
。public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
实例:
public class ThreadTest { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(4); for (int i = 0; i < 100; i++) { service.execute(new Task()); } } } class Task implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " => 正在运行..."); } }
Executors.newSingleThreadExecutor
:单线程化的线程池
-
简介:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
-
适用:一个任务一个任务执行的场景
-
构造函数:
corePoolSize
和maximumPoolSize
: 1keepAliveTime
:0L
(不限时:存活时间无限)unit
:TimeUnit.MILLISECONDS
workQueue
:LinkedBlockingQueue
该无界队列容量是Integer.MAX_VALUE
,吞吐量通常要高于ArrayBlockingQueue
。由于没有上限,当任务越来越多,并且无法及时处理完毕,也就是请求堆积越来越多,会容易造成占用大量的内存,会导致OOM
。public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
实例:
public class ThreadPoolTest { public static void main(String[] args) { ExecutorService service = Executors.newSingleThreadExecutor(); for (int i = 0; i < 100; i++) { service.execute(new SingleTask()); } } } class SingleTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " => 正在运行..."); } }
Excutors.newCachedThreadPool
:无界线程池,可以回收多余的线程。
-
简介:当有新任务到来,则插入到
SynchronousQueue
中,由于SynchronousQueue
是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。 -
适用:执行很多短期异步的小程序或者负载较轻的服务
-
构造函数
corePoolSize
:0maximumPoolSize
:Integer.MAX_VALUE
最大,可能会创建数量非常多的线程,甚至导致OOM
。keepAliveTime
:60L
(不限时:存活时间无限)unit
:TimeUnit.SECONDS
(秒)workQueue
:SynchronousQueue
直接交换队列,容量为0,任务来了直接交给线程池,所有会创建很多现线程,60S之后会收回。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
实例:
public class ThreadPoolCacheTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { service.execute(new CacheTask()); } } } class CacheTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " => 正在运行..."); } }
Excutors.newScheduledThreadPool
:
-
简介:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入
DelayedWorkQueue
队列中,这是一种按照超时时间排序的队列结构。 -
适用:周期性执行任务的场景
-
构造函数:
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
-
实例:
public class ThreadPoolScheduledTest { public static void main(String[] args) { ScheduledExecutorService service = Executors.newScheduledThreadPool(5); // 五秒之后执行 service.schedule(new SingleTask(), 5, TimeUnit.SECONDS); // 五秒之后执行,之后每五秒执行一次 service.scheduleAtFixedRate(new ScheduledTask(), 5, 5, TimeUnit.SECONDS); for (int i = 0; i < 1000; i++) { service.execute(new CacheTask()); } } } class ScheduledTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " => 正在运行..."); } }
3. 合理设置线程池中线程数量
- CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右
- 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍,以
JVM
线程监控显示繁忙情况为依据,保证线程空闲可以衔接上 - 线程数 = CPU核心数 * (1 + 平均等待时间 / 平均工作时间)
4. 线程池的停止
五个停止线程池的相关方法:
shutdown
:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池shutdownNow
:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行isShutdown
:返回Boolean
,标识线程池是否进入停止的状态isTerminated
:返回Boolean
,线程池是否完全停止。awaitTermination
:测试一段时间内线程池是否停止。
三、线程池拒绝策略
3.1. 线程池拒绝时机:
- 当
Executor
关闭时,提交新任务会被拒绝 - 以及当
Executor
对最大线程和工作容量使用有限边界并且已经饱和时。
3.2. 拒绝策略
AbortPolicy
:直接抛出RejectedExecutionException
异常,也不执行这个任务了DiscardPolicy
:会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。DiscardOldestPolicy
:当任务呗拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。CallerRunsPolicy
:任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
四、对线程池的装饰
我们可以继承ThreadPoolExecutor
这个类,来实现我们的一些其他的操作,例如在线程日志、统计等线程池不具备的功能。也可以看ThreadPoolExecutor
这个类源码,注释中就有。还是多看源码啊,哈哈!
public class TestPools extends ThreadPoolExecutor {
// 暂停的标志
private boolean isPaused;
// 锁
private ReentrantLock lock = new ReentrantLock();
private Condition unPaused = lock.newCondition();
// 构造函数
public TestPools(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
// 可以再线程执行任务之前做一些处理
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
lock.lock();
try {
while(isPaused) {
unPaused.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 唤醒,可将暂停的线程池唤醒
*/
public void resume() {
lock.lock();
try {
isPaused = false;
unPaused.signalAll();
} finally {
lock.unlock();
}
}
/**
* 暂停,将线程池暂停
*/
private void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
// 测试代码
public static void main(String[] args) throws InterruptedException {
TestPools pools = new TestPools(20, 20, 10L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
for (int i = 0; i < 1000; i++) {
pools.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " => 正在执行...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
Thread.sleep(1500);
pools.pause();
System.out.println("线程池暂停");
Thread.sleep(1500);
System.out.println("线程池唤醒");
pools.resume();
pools.shutdown();
}
}
五、线程池实现原理简单分析
5.1. 线程池的组成部分:
- 线程池管理器
- 工作线程
- 任务队列
- 任务接口
5.2. 工作流程
当一个任务提交至线程池之后:
1、线程池首先判断***核心线程池***里面的线程是否已经满了,如果没有满,则创建一个新的工作线程来执行线程,否则到第2步;
2、判断***工作队列***是否已经满了,如果没有满,将线程放入工作队列,否则到第3步;
3、判断***线程池***里最大线程数是否已经满了,没有满则创建一个新的工作线程来执行,否则交给拒绝策略来处理任务。
5.2. 线程池的内部状态控制变量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
AtomicInteger
变量ctl
(1110 0000 0000 0000 0000 0000 0000 0000)中低29位做线程数,高3位做线程池状态。
5.3. 线程池状态转换:
5.4. 任务执行流程跟踪:
先弄一个测试代码:
public class TestPoolOne {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(4), Executors.defaultThreadFactory());
for (int i = 0; i < 10; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " - 正在执行...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
我们将新任务通过execute(Runnable command)
方法进行提交
execute
执行流程:
ctl
初始值:1110 0000 0000 0000 0000 0000 0000 0000,低29位是0,所有线程池容量初始值0,capacity
:0001 1111 1111 1111 1111 1111 1111 1111, workerCountOf
计算线程池现在线程的数量,初始的时候是0。
/**
* 在将来的某个时间执行给定的任务。 任务可以在新线程或现有池线程中执行。
*
* 如果由于该执行程序已关闭或已达到其容量而无法提交执行任务,
* 则该任务由当前的拒绝策略处理。
*
* @param command 要执行的任务
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
// 任务为空,抛出空指针异常
if (command == null)
throw new NullPointerException();
/*
* 进行3个步骤:
* 1. 如果正在运行的线程少于corePoolSize线程,请尝试使用给定命令作为其
* 第一个任务来启动新线程。对addWorker的调用原子地检查runState和workerCount,
* 从而通过返回false来防止在不应该添加线程的情况下发出错误警报。
* 2. 如果任务可以成功排队,那么我们仍然需要仔细检查是否应该添加线程
* (因为现有线程自上次检查后就死掉了)或自进入此方法以来该池已关闭。
* 因此,我们重新检查状态,并在必要时回滚排队,如果已停止,或者在没有线程的情况下启动新线程。
* 3. 如果我们无法将任务排队,则尝试添加一个新线程。如果失败,
* 我们知道我们已关闭或已饱和,因此拒绝该任务。
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
addWorker
方法,主要负责创建新的线程并执行任务,线程池创建执行任务时,需要获取全局锁:
private boolean addWorker(Runnable firstTask, boolean core) {
/* 当线程池处于 RUNNING (运行)状态时,只有在线程池中的有效线程数
* 被成功加一以后,才会退出该循环而去执行后边的代码。
* 也就是说当线程池在 RUNNING (运行)状态下退出该 retry 循环时, 线程池中的有效线程数
* 一定少于此次设定的最大线程数(可能是 corePoolSize 或 maximumPoolSize)。
*
* retry 是个为了跳出多层循环.
* break retry; 跳出循环之后不会再次进入循环
* continue retry; 会跳出内层循环,从外层循环再次开始
*/
retry:
// for 和 while 之间啥区别,C语言早之前while会多调用几条汇编指令,现在Java优化已经一样了
for (;;) {
int c = ctl.get();
// 获取当前线程池状态
int rs = runStateOf(c);
/* 表示没有创建新线程, 新提交的任务也没有被执行.
* 线程池满足如下条件中的任意一种时, 就会直接结束该方法, 并且返回 false
* (1) 处于STOP/TIDYING/TERMINATED转态
* (2) 处于SHUTDOWN状态并且参数fistTask不为null
* (3) 处于SHUTDOWN状态并且阻塞队列workQueue为空
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 计算线程池个数
int wc = workerCountOf(c);
/* 如果线程池内的有效线程数大于或等于了理论上的最大容量 CAPACITY 或者实际
* 设定的最大容量, 就返回 false, 直接结束该方法. 这样同样没有创建新线程,
* 新提交的任务也同样未被执行。
* core => true 最大容量为corePoolSize,否则为maximumPoolSize
*/
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 有效线程数加一
if (compareAndIncrementWorkerCount(c))
// 跳出多层循环,执行后面的代码
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 根据参数 firstTask来创建 Worker对象 w。具体看一下下面的Worker中的分析
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
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) {
// 将线程启动,看Worker下的run方法
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
接着看一下Worker
线程池中的内部类
构造函数
Worker(Runnable firstTask) {
// 禁止中断,直到runWorker
setState(-1);
this.firstTask = firstTask;
// 创建新线程
this.thread = getThreadFactory().newThread(this);
}
运行线程
/* Worker运行循环。反复从队列中获取任务并执行它们,同时解决许多问题:
*
* 1. 我们可以从初始任务开始,在这种情况下,我们不需要获取第一个任务。
* 否则,只要池正在运行,我们就会从getTask获取任务。
* 如果返回null,则工作程序将由于更改的池状态或配置参数而退出。
* 其他退出是由于外部代码中的异常引发而导致的,在这种情况下completedAbruptly成立,
* 这通常导致processWorkerExit替换此线程。
*
* 2. 在运行任何任务之前,先获取锁,以防止任务执行时其他池中断,
* 然后确保除非池正在停止,否则此线程不会设置其中断。
*
* 3. 每个任务运行之前都会调用beforeExecute,
* 这可能会引发异常,在这种情况下,我们将导致线程死掉
* (中断,带有completelyAbruptly true的循环),而不处理该任务。
*
* 4. 假设beforeExecute正常完成,我们运行任务,收集其引发的任何异常以发送给afterExecute。
* 我们分别处理RuntimeException,Error(规范保证我们可以捕获它们)和任意Throwables。
* 因为我们无法在Throwable.run中抛出Throwable,所以我们将它们包装在Errors中
* (输出到线程的UncaughtExceptionHandler)。任何抛出的异常也会保守地导致线程死亡。
*
* 5. task.run完成后,我们调用afterExecute,这可能还会引发异常,这也会导致线程死亡。
* 根据JLS Sec 14.20,此异常是即使task.run抛出也会生效的异常。
*
* 异常机制的最终结果是afterExecute和线程的
* UncaughtExceptionHandler具有与我们所能提供的有关用户代码遇到的任何问题一样准确的信息。
*/
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
获取阻塞队列中的任务:
/* 根据当前配置设置执行阻塞或定时等待任务,或者如果此工作程序由于以下任何原因而必须退出,则返回null
*
* 1.
*
* 2. 线程池停止
*
* 3. 线程池关闭和队列为空
*
* 4. 该工作程序超时等待任务,并且在定时等待之前和之后均会终止工作
* (即{@code allowCoreThreadTimeOut || workerCount> corePoolSize})
* 并且如果队列为非空,此工作程序不是池中的最后一个线程。
*/
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 如果线程池已停止, 或者线程池被关闭并且线程池内的阻塞队列为空, 则结束该方法并返回 null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
六、线程池状态和注意
6.1. 线程池的状态
running
:排队任务shutdown
:不接受新任务,但处理排队任务stop
:不接受新任务,也不处理排队任务,并中断正在进行的任务tidying
:所有任务都已经终止,workerCount
为零时,线程会转换到tidying
状态,并将运行terminate()
钩子方法terminated
:terminate()
运行完成。
6.2. 使用线程池需要注意:
- 避免任务堆积
- 避免线程数过度增加
- 排查线程泄露(业务逻辑问题,线程无法结束)