目录
1、是什么?
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
2、为什么?
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
3、怎么用?
3.1 创建
方式一:使用JDK中Executors工具类4种方式创建(不推荐)
// 1.单个线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2.固定线程数的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(corePoolSize);
// 3.可缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 4. 实现调度的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(corePoolSize);
...
通过JDK提供的Executors工具类创建的几种方式都不推荐,因为底层使用的阻塞队列或者是最大线程数都是Integer的最大值,可能会导致OOM。
方式二:实际项目中使用线程池按照阿里巴巴开发手册中推荐的创建线程池的方式(推荐)
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:允许的创建线程数
量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
// 使用ThreadPoolExecutor几种带参数的构造方法创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(....);
3.2 线程池参数含义
ThreadPoolExecutor中提供了多种重载的构造函数,其中的七大参数的含义,我们可以在JDK中ThreadPoolExecutor类中的方法源码的注释中看到:
/**
* 给定初始化参数的创建一个新的线程池ThreadPoolExecutor
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* (1)corePoolSize:核心线程数,线程池中要保留的最小线程数,即使他们都是空闲状态,
* 也不会被清理,除非设置了allowCoreThreadTimeOut=true
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
*
* (2)maximumPoolSize:最大线程数,线程池中允许创建的最大线程数量
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
*
* (3)keepAliveTime:保留存活时间,当线程数量大于核心线程数,这些超出的空闲状态的线程
* 在线程池中等待新任务到来的所能存活的最大时间
* @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.
*
* (4)unit:时间单位,keepAliveTime 参数的时间单位
* @param unit the time unit for the {@code keepAliveTime} argument
*
* (5)workQueue:工作队列,这个队列被用来保存被执行前的任务,这个队列只会保存
* 被提交的 Runnable状态的任务。
* @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.
*
* (6)threadFactory:当任务被执行时用来创建一个新的线程的工厂
* @param threadFactory the factory to use when the executor
* creates a new thread
*
* (7)handler:拒绝策略,当因为达到线程池最大数量并且等待队列容量也满了的时候,
* 处理被阻塞的新添加的任务策略
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @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}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
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;
}
3.3 线程池任务的执行过程
当用户提交一个任务到线程池中,是如何被执行的呢?所有任务最终都会调用execute方法来完成。
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @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();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
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);
}
通过查看代码和注释信息,我们可以简单总结下任务被提交到线程池后的执行过程大致是:
- 首先检查线程池的状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务;
- 如果工作线程数小于核心线程数,则创建并启动一个新线程来执行新提交的任务,即使现在有空闲的核心线程,也会创建新线程;
- 如果工作线程数大于或等于核心线程数,且线程池内的工作等待队列未满,则将任务添加到该阻塞队列中;
- 如果工作线程数大于或等于核心线程数,并且工作线程数小于最大线程数,且工作队列已满,则创建并启动一个新线程来执行新提交的任务; 5. 如果工作线程数大于或等于最大线程数,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常;
流程图如下:
3.4 如何设置线程池的参数
具体的线程池参数配置要根据业务场景需要来配置,并没有统一的配置参数;其中线程池最核心的参数就是线程池中的线程数量,一种比较广泛使用的方式是:
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
4、总结
有关线程池的设计细节还有很多,比如线程池的状态和线程数是通过AtomicInteger原子类中的一个int值来存储的,要想了解线程池的具体实现细节,最好的办法就是去查看JDK中的源码和注释,里面有详细的解析,本文只是简单总结一下线程池的常见知识。
参考地址:
JDK源码
Java线程池实现原理及其在美团业务中的实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
三分钟弄懂线程池执行过程:https://juejin.cn/post/6866054685044768782