在开发中,合理地使用线程池能带来3个好处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行。
第三:提高线程的可管理性。线程时稀缺资源,如果无限制地创建,不仅回消耗系统资源,还会降低系统地稳定性,使用线程池可以进行统一分配、调优和监控。
线程池地实现原理
从上图可以看出,当提交一个新的任务到线程池时,线程池的处理流程如下
- 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务存储在这个工作队列。如果工作队列满了,则进入下个流程。
- 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和粗略来处理这个任务。
ThreadPoolExecutor 执行 execute() 方法的示意图如下 - 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁)
- 如果运行的线程等于或多余 corePoolSize,则将任务加入 BlockingQueue
- 如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步需要获取全局锁)
- 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法
ThreadPoolExecutor 采用上述步骤的总体设计思路,是为了在执行 execut() 方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在 ThreadPoolExecutor 完成预热之后(当前运行地线程数大于等于 corePoolSize),几乎所有地 execute() 方法调用都是执行步骤2,而步骤2不需要获取全局锁。
源码分析(Android API 29):
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);
}
线程池地使用
线程池的创建
可以通过 ThreadPoolExecutor 来创建一个线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
- corePoolSize (线程池地基本大小):如果调用了线程池地 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有基本线程
- workQueue (任务队列):用于保存等待执行任务地阻塞队列。可以选择以下几个阻塞队列。
- ArrayBlockingQueue:一个基于数组结构地有界阻塞队列,此队列按 FIFO (先进先出)原则对元素进行排序
- LinkedBlockingQueue:一个基于链表结构地阻塞队列,按照 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。Executors.newFixedThreadPool() 使用了这个队列
- SynchronousQueue:一个不存储元素地阻塞队列。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于 LinkedBlockingQueue。Executors.newCachedThreadPool() 使用了这个队列
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列
- maximumPoolSize (线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
- ThreadFactory:用于设置创建线程的工厂
- RejecteExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。在 JDK1.5 中Java线程池框架提供了4种策略。
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOlderstPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
- keepAliveTime(线程活动保持时间):如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率
- TimeUnit(线程活动保持时间的单位)
向线程池提交任务
可以使用两个方法向线程池提交任务,分别是 execute() 和 submit() 方法。
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit() 方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get 方法来获取返回值,get 方法会阻塞当前线程直到任务完成。
关闭线程池
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池种的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 先将线程池的状态设置为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN,然后中断所有没有正在执行任务的线程。
合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度分析。
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
- 任务的优先级:高、中和低
- 任务的执行时间:长、中和短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接 CPU密集型任务应配置尽可能小的线程,如配置 CPU数量 + 1 个线程的线程池。由于 IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2 * CPU数量。可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数。
线程池的监控
可以通过线程池提供的参数进行监控
- taskCount:线程池需要执行的任务数量
- completedTaskCount:线程池在运行过程中已完成的任务数
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过
- getPoolSize:线程池的线程数量
- getActiveCount:获取活动的线程数