一、线程池参数为什么如此重要?
在高并发场景下,线程池的参数配置直接影响系统的吞吐量、响应时间和稳定性。线程池使用面临的核心的问题在于:线程池的参数并不好配置 。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO
密集型和CPU
密集型的任务运行起来的情况差异非常大错误的配置可能导致:
- 资源浪费:线程数过多引发
CPU
上下文切换开销剧增; - 请求堆积:队列过长导致超时雪崩;
- 服务降级:线程数不足引发请求拒绝
RejectExecutionException
;
二、线程池参数公认
✅先说结论:线程池设置多大,并没有固定答案, 需要结合实际情况不断的测试才能得出最准确的数据。
1. 基础默认:Little Law
java
线程数(N)= QPS x (任务平均执行时间 / 1000)
QPS
:每秒请求量 (如:A接口每秒访问量为5000
,那边A接口的QPS
即为5000
);- 任务平均执行时间
(Task Avg RT)
:单个任务从提交到完成的时间(单位:毫秒);
2. 考虑任务类型
CPU
密集型(如计算哈希):线程数 ≈ CPU核心数 + 1;IO
密集型(如数据库查询):线程数 = CPU核心数 x (1 + 等待时间 / 计算时间);
举个例子:某订单处理任务平均执行时间为:50ms
,其中30ms
为数据库等待时间,那么合理的线程数为:
线程数 = 8核 x (1 + 30/20) = 20;
3. 业界的一些线程池参数配置方案

4. 实际生产案例:物联网充电桩C端扫码进入业务处理线程池调优
场景描述:
- 目标QPS:8000
- 任务类型:IO密集型(含数据库操作)
- 单任务平均耗时:约80ms(含50ms数据库等待)
参考计算:
scss
理论线程数 = 8000 × (80/1000) = 640;
实际线程数 = CPU核心数 × (1 + 等待时间 / 计算时间) = 32核 × (1 + 50/30) ≈ 85;
最终配置:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
85, // 核心线程数
150, // 最大线程数(应对瞬时峰值)
60, // 空闲线程存活时间(秒)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000), // 队列容量
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
压测结果(注意:非百分百精确,不同机器结果不同):
配置版本对比 | QPS | 平均响应时间 | 错误率 | CPU利用率 |
---|---|---|---|---|
优化前 | 4200 | 120ms | 15% | 78% |
优化后 | 8500 | 75ms | 0.2% | 56% |
5. 结论
实际上并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型`相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。
三、线程池核心设计与实现
线程池是一种通过"池化"思想,帮助我们管理线程而获取并发性的工具,在Java
中的体现是ThreadPoolExecutor
类,想深入理解框架板块的知识点,必须是从框架的底层实现原理入手,层层深入,线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为解决资源分配这个问题,线程池采用了"池化"(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
①ThreadPoolExecutor参数含义
coolPoolSize:线程池核心线程数大小
是线程池中的一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut
,简单来说线程池分为两个部分,核心线程池和非核心线程池,核心线程池中的线程一旦创建便不会被销毁,非核心线程池中的线程在创建后如果长时间没有被使用则会被销毁。
maximunPoolSize:线程池最大线程数量
整个线程池的大小,此值大于等于1。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize
指定。工作队列满,且线程数等于最大线程数,此时再提交任务则会调用拒绝策略。maximumPoolSize
- corePoolSize
= 非核心线程池的大小。
keepAliveTime:多余的空闲线程存活时间
非核心线程池中的线程在keepAliveTime
时间内没有被使用就会被销毁,时间单位由TimeUnit unit
决定。
当线程空闲时间达到keepAliveTime
值时,多余的线程会被销毁直到只剩下corePoolSize
个线程为止。
TimeUnit unit:空闲线程存活时间单位
keepAliveTime
的计量单位
BlockingQueue workQueue:任务队列
阻塞队列用来存储任务,当有新的请求线程处理时,如果核心线程池已满,新来的任务会放入workQueue
中,等待线程处理,JUC
提供的阻塞队列有很多,例如:ArrayBlockingQueue
,LinkedBlockingQueue
,PriorityBlockingQueue
,SynchronousQueue
等。
ThreadFactory:工厂类对象
线程池的创建传入了此参数,是通过工厂类中的newThread()方法来实现的。
RejectedExecutionHandler handler:拒绝策略
如果线程池中没有空闲线程,已存在maximumPoolSize
个线程,且阻塞队列workQueue
已满,这时再有新的任务请求线程池执行,会触发线程池的拒绝策略,可以通过参数handler
来设置拒绝策略,注意只有有界队列例如:ArrayBlockingQueue
或者指定大小的 LinkedBlockingQueue
等拒绝策略才有用,因为无解队列拒绝策略永远不会被触发。
②ThreadPoolExecutor运行流程

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
1️⃣ 直接申请线程执行该任务;
2️⃣ 缓冲到队列中等待线程执行;
3️⃣ 拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
③任务执行机制
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
首先,所有任务的调度都是由execute
方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
- 首先检测线程池运行状态,如果不是
RUNNING
,则直接拒绝,线程池要保证在RUNNING
的状态下执行任务。 - 如果
workerCount
<corePoolSize
,则创建并启动一个线程来执行新提交的任务。 - 如果
workerCount
>=corePoolSize
,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。 - 如果
workerCount
>=corePoolSize
&&workerCount
<maximumPoolSize
,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。 - 如果
workerCount
>=maximumPoolSize
,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

④任务缓冲机制
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列BlockingQueue
是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

⑤任务拒绝策略
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize
时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计如下:
csharp
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:
策略名称 | 描述 |
---|---|
ThreadPoolExecutor.AbortPolicy |
丢弃任务并抛出RejectedExecutionException 异常,这线程池默认的拒绝策略,抛出异常,并及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。 |
ThreadPoolExecutor.DiscardPolicy |
丢弃任务,但是不抛出异常,使用此策略,可能会使我们无法发现系统的异常状态,建议一些无关紧要的业务采用此策略。 |
ThreadPoolExecutor.DiscardOldestPolicy |
丢弃队列最前面的任务,然后重新提交被拒绝的任务,是否要采用这种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。 |
ThreadPoolExecutor.CallerRunsPolicy |
由调度线程(提交任务的线程)处理该任务,这种情况是需要让所有的任务都执行完毕,这种策略适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。 |
csharp
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
/**
* 只要线程池没有被关闭,那么由提交任务的线程自己来执行这个任务
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
/**
* 不管怎样,直接抛出RejectedExecutionException 异常,是线程池默认的策略,
* 如果在自定义线程池的时候不传相应的handler的话,那么就会使用这个拒绝策略。
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
/**
* 不做任何处理,直接忽略掉这个任务
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
/**
* 如果线程池没有被关闭的话,把队列头的任务(也就是等待了最长时间的)直接扔掉,然后提交这个任务到仍待队列中
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
⑥核心源码分析-ThreadPoolExecutor
ThreadPoolExecutor的execute方法
scss
// 线程池状态和线程数整数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取线程池状态和线程数
int c = ctl.get();
// 如果当前线程数小于核心线程数,创建Worker线程并启动线程
if (workerCountOf(c) < corePoolSize) {
// 添加任务成功,到这里就结束了,结果会包装到FutureTask中
if (addWorker(command, true))
return;
c = ctl.get();
}
// 要么当线程数大于等于核心线程数,要么刚刚addWorker失败了,如果线程池处理RUNNING状态,会把这个任务添加到任务队列中workQueue中
if (isRunning(c) && workQueue.offer(command)) {
// 二次状态检查
int recheck = ctl.get();
// 如果线程池已不处于RUNNING状态,那么移除已经入队列的这个任务,并且执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池还是RUNNING状态,并行线程数为0,重新创建一个新的线程,这个的目的是担心任务提交的到队列中了,但是线程都关闭了
else if (workerCountOf(recheck) == 0)
// 创建Worker,并启动里面的Thread,为什么传null,线程启动后会自动从阻塞队列拉取任务执行
addWorker(null, false);
}
// 如果workQueue队列满了,那么进入到这个分支,以maximumPoolSize为界创建新的worker线程并启动,如果失败,说明当前线程数已经达到maximumPoolSize,并执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
ThreadPoolExecutor的Worker
来看下线程Worker
作为线程池中真正执行任务的线程,继承了抽象类AbstractQueuedSynchronizer
,用AQS
来实现独占锁,为的就是实现不可重入的特性去反应线程现在的执行状态。
scala
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
/** 真正的线程 */
final Thread thread;
/** 这里的Runnable是任务,这个线程起来逸煌需要执行的第一个任务,第一个任务就是存放在这里的(注意:线程可不止执行这一个任务) */
Runnable firstTask;
/** 用于存放此线程完成的任务数,注意,这里使用了volatile,保证可见性 */
volatile long completedTasks;
// TODO: switch to AbstractQueuedLongSynchronizer and move
// completedTasks into the lock word.
/**
* Worker只有一个构造方法,传入firstTask也可以传如null
*/
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
// 调用 ThreadFactory来创建一个新的线程
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker. */
public void run() {
runWorker(this);
}
}
ThreadPoolExecutor
的execute
方法中创建Worker
就是调用了下面的addWorker
方法,该方法 2 个入参。第一个参数是准备提交给这个线程执行的任务,当为null
时,线程启动后会自动从阻塞队列拉任务执行。
第二个参数为true
代表使用核心线程数作为创建线程的界限,也就说创建这个线程的时候,如果线程池中的线程总数已经达到核心线程数,那么不能响应这次创建线程的请求 如果是false
,代表使用最大线程数 作为界限同理。
ini
// 这个是真正的线程
final Thread thread;
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 当线程池处于 SHUTDOWN 的时候,不允许提交任务,但是已有的任务继续执行
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务了 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 重试 continue retry;
if (compareAndIncrementWorkerCount(c))
break retry;
// 由于有并发,重新再读取一下 ctl
c = ctl.get();
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// worker 是否已经启动
boolean workerStarted = false;
// 是否已将这个 worker 添加到 workers 这个 HashSet 中
boolean workerAdded = false;
Worker w = null;
// 可以开始创建线程来执行任务了
try {
// 把 firstTask 传给 worker 的构造方法
w = new Worker(firstTask);
// 取 worker 中的线程对象,之前说了,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程
final Thread t = w.thread;
if (t != null) {
// 整个线程池的全局锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// 小于 SHUTTDOWN 那就是 RUNNING
if (rs < SHUTDOWN ||
// 如果等于 SHUTDOWN,前面说了,不接受新的任务,但是会继续执行等待队列中的任务
(rs == SHUTDOWN && firstTask == null)) {
// worker 里面的 thread 可不能是已经启动的
if (t.isAlive())
throw new IllegalThreadStateException();
// 加到 workers 这个 HashSet 中
workers.add(w);
int s = workers.size();
// largestPoolSize 用于记录 workers 中的个数的最大值
if (s > largestPoolSize)
// 因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 添加成功的话,启动这个线程
if (workerAdded) {
// 启动线程
t.start();
workerStarted = true;
}
}
} finally {
// 如果线程没有启动,需要做一些清理工作,如前面 workCount 加了 1,将其减掉
if (! workerStarted)
addWorkerFailed(w);
}
// 返回线程是否启动成功
return workerStarted;
}
Worker
线程真正的执行逻辑为runWorker
方法实现如下:
ini
// 此方法由 worker 线程启动后调用,这里用一个 while 循环来不断地从等待队列中获取任务并执行
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// worker 在初始化的时候,可以指定 firstTask,那么第一个任务也就可以不需要从队列中获取
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 循环调用 getTask 获取任务
while (task != null || (task = getTask()) != null) {
w.lock();
// 如果线程池状态大于等于 STOP,那么意味着该线程也要中断
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) {
// 这里不允许抛出 Throwable,所以转换为 Error
thrown = x; throw new Error(x);
} finally {
// 钩子方法,将 task 和异常作为参数,留给需要的子类实现
afterExecute(task, thrown);
}
} finally {
// 置空 task,准备 getTask 获取下一个任务
task = null;
// 累加完成的任务数
w.completedTasks++;
// 释放掉 worker 的独占锁
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 获取不到任务时,主动回收自己 执行线程关闭 可能getTask 返回 null,也就是说,队列中已经没有任务需要执行了,执行关闭,或者任务执行过程中发生了异常.
processWorkerExit(w, completedAbruptly);
}
简单来说Worker
线程启动后调用,会通过while
循环来不断地通过getTask
方法从等待队列中获取任务并执行达到线程回收。
getTask
的实现也比较简单,阻塞直到获取到任务返回,keepAliveTime
超时退出。
ini
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
// 获取线程池的状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// CAS 操作,减少工作线程数
decrementWorkerCount();
return null;
}
// 获取线程池中的线程数
int wc = workerCountOf(c);
// 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 到 workQueue 中获取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
// 如果此 worker 发生了中断,采取的方案是重试
// 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量,那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null
timedOut = false;
}
}
💥 需要注意的是:
线程池处于 SHUTDOWN
,而且 workQueue
是空的,该方法返回 null
,这种不再接受新的任务。
线程池中有大于 maximumPoolSize
个 workers
存在,这种可能是因为有可能开发者调用了 setMaximumPoolSize()
将线程池的 maximumPoolSize
调小了,那么多余的 Worker
就需要被关闭。
线程池处于 STOP
,不仅不接受新的线程,连 workQueue
中的线程也不再执行。
如果此 worker
发生了中断,采取的方案是重试,也就是说如果开发者将 maximumPoolSize
调小了,导致其小于当前的 workers
数量,那么意味着超出的部分线程要被关闭。重新进入 for
循环获取任务。
java
public void setMaximumPoolSize(int maximumPoolSize) {
if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
throw new IllegalArgumentException();
this.maximumPoolSize = maximumPoolSize;
if (workerCountOf(ctl.get()) > maximumPoolSize)
// 中断 worker 重试 超出的部分线程要被关闭
interruptIdleWorkers();
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
四、避坑指南:线程池配置的几大误区💥
简单列举一下我自己的日常开发中亲身踩过的一些坑,以及如何并没这些坑:
1. 直接使用Executors
许多初学者在创建线程池时,直接使用Executors
提供的快捷方法:
ini
ExecutorService executor = Executors.newFixedThreadPool(10);
Executors创建返回线程池的弊端:
FixedThreadPool
和SingleThreadPool
:允许的请求队列长度是Integer.MAX_VALUE
,可能会堆积大量的请求 ,导致OOM
。CachedThreadPool
和SchduledThreadPool
:允许的场景线程数量为Integer.MAX_VALUE
,可能会创建大量的线程 ,导致OOM
。
直接使用问题在哪?
- 无界队列:
newFixedThreadPool
使用的队列是LinkedBlockingQueue
,它是无界队列,任务堆积可能会导致内存溢出。 - 线程无限增长:
newCachedThreadPool
会无限创建线程,在任务量激增时可能耗尽系统资源。
示例:内存溢出的风险
ini
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000000; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
💥当任务数远大于线程数的时候,导致任务无无限堆积在队列中,最终导致内存溢出OutOfMemoryError
。
解决办法
使用ThreadPoolExecutor
,并明确指定参数:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
2. 错误配置导致的线程过载
一些初级开发者随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费,比如我实际看到过这样配置:
ini
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
100, // 最大线程数
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)
);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
Thread.sleep(5000); // 模拟耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
❌这种配置带来的后果:在任务激增时,会创建大量线程,系统资源被耗尽。
正确配置方式
根据任务类型选择合理的线程数:
CPU
密集型:线程数建议设置为CPU
核心数 + 1。IO
密集型:线程数建议设置为2 * CPU
核心数。
正确示例
ini
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCores + 1,
cpuCores + 1,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50)
);
3. 忽略任务队列的选择
任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。
日常开发中常见队列的坑
- 无界队列
Boundless Queue
:任务无限堆积; - 有界队列
Bounded Queue
:队列满了会触发拒绝策略; - 优先级队列
Priority Queue
:容易导致高优先级任务频繁抢占低优先级任务。
4. 忽略拒绝策略:拒绝策略设置错误导致接口超时
线程池拒绝策略可以说一个常见八股文问题。可能基本上都能记住了线程池有四种决绝策略,可是实际代码编写中,发现大多数人都只会用CallerRunsPolicy
策略(由调用线程处理任务)。我吃过这个亏,因此也拿出来讲讲。
4.1. 问题原因
曾经有一个线上业务接口使用了线程池进行第三方接口调用,线程池配置里的拒绝策略采用的是CallerRunsPolicy
。示例代码如下:
java
// 某个线上线程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
20, // 最小核心线程数
50, // 最大线程数,当队列满时,能创建的最大线程数
60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
new CustomizableThreadFactory("task"), // 自定义线程名
new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
);
threadPoolExecutor.execute(() -> {
// 调用第三方接口
...
});
在第三方接口异常的情况下,线程池任务调用第三方接口一直超时,导致核心线程数、最大线程数堆积被占满、阻塞队列也被占满的情况下,也就会执行拒绝策略,但是由于使用的是CallerRunsPolicy
策略,导致线程任务直接由我们的业务线程来执行。
因为第三方接口异常,所以业务线程执行也会继继续超时,线上服务采用的Tomcat
容器,最终也就导致Tomcat
的最大线程数也被占满,进而无法继续向外提供服务。
4.2. 解决方法
首先我们要考虑业务接口的可用性,就算线程池任务被丢弃,也不应该影响业务接口。
在业务接口稳定性得到保证的情况下,在考虑到线程池任务的重要性,不是很重要的话,可以使用DiscardPolicy
策略直接丢弃,要是很重要,可以考虑使用消息队列来替换线程池。
5. 阻塞任务占用线程池
如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。
比如:
scss
executor.submit(() -> {
Thread.sleep(10000); // 模拟阻塞任务
});
5.1. 改进方法
- 减少任务的阻塞时间。
- 增加核心线程数。
- 使用异步非阻塞方式,如
NIO
。
6. 未实现动态调整线程池参数
ini
executor.setCorePoolSize(20);
executor.setMaximumPoolSize(50);
建议走配置动态配置线程池,比如通过项目本身yml/properties
配置,或者借助配置中心配置,比如:Nacos
、Apollo
,也可以集成第三方场插件,比如:DynamicTp。
五、总结
实际上线程池的整体架构设计思想是值得我们深入研究和学习的,现如今很多架构上都借鉴了"池化"的设思想,比如:数据库连接池,Tomcat
线程池,业务开发中的线程池,Netty
的EventLoopGroup
,甚至于HTTP
的长连接也是复用TCP
连接,和池化思想很相似。好了,今天的分享就到此结束了,如果文章对你有所帮助,欢迎:点赞👍+评论💬+收藏❤ ,我是:IT_sunshine
,我们下期见!