Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处 。
-
第一:降低资源消耗。避免重复创建和销毁造成的消耗。
-
第二:提高响应速度。当任务到达时,不需要再次创建线程就能立即执行。
-
第三:提高线程的可管理性。可以进行统一分配、调优和监控。
线程池的主要处理流程
从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。
-
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作
线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。 -
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这
个工作队列里。如果工作队列满了,则进入下个流程。 -
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程
来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor执行execute()方法的示意图
ThreadPoolExecutor执行execute方法分下面4种情况。
- 1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
- 2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
- 3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
- 4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用
RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
源码分析:上面的流程分析让我们很直观地了解了线程池的工作原理,让我们再通过源代
码来看看是如何实现的,线程池执行任务的方法如下。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/**
* 如果运行的线程小于corePoolSize,则尝试用给定的命令作为第一个任务启动一个新线程。
* 对addWorker的调用原子性地检查runState和workerCount,因此可以通过返回false来防止错误警报,因为错误警报会在不应该添加线程的时候添加线程。
*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程池处于RUNNING状态,并将任务放入workQueue队列
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);
}
线程池中的线程执行任务分两种情况
- 1)在execute()方法中创建一个线程时,会让这个线程执行当前任务。
- 2)这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。
ThreadPoolExecutor中线程执行任务的示意图如图 :
未完待续