Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行的程序都可以使用线程池。
合理使用线程池能带来三个好处:
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
线程池的实现原理
当提交一个任务时,线程池的处理流程如下
很清晰的一幅图,简洁明了
- 我们提交一个任务到线程池时,线程池首先判断当前核心线程池是否已满,如果没有满的话,那么就直接创建一个新的线程来执行任务;如果核心线程池已满的话那么就去判断队列是否已满;
- 如果工作队列没有满的话,那么就将这个任务存储到工作队列中进行等待;如果没满的话,那么就去判断线程池是否已满;
- 如果线程池没有满的话,同样创建一个新的线程去执行任务;相反,如果线程池满的话那么就去按照饱和策略处理无法执行的任务。
ThreadPoolExcutor执行excute()方法的示意图
ThreadPoolExcute执行excute方法分下面四种情况:
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁);
- 如果运行的线程等于corePoolSize,则将任务加入BlockingQueue;
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(同上,执行这一步需要获取全局锁);
- 如果创建新线程将使当前运行的线程 超出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);
}
工作线程:线程池创建线程时,会将线程粉装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行。我们可以从Worker类的run()方法里看到。
public void run(){
try{
Runnable task = firstTask;
firstTask = null;
while(task != null || (task = getTask()) != null){
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
ThreadPoolExecutor中线程执行任务的示意图如图
线程池中的线程执行任务分两种情况,如下:
- 在execute()方法中创建一个线程时,会让这个线程执行当前任务;
- 这个线程执行完图中1的任务后,会反复从BlockingQueue获取任务来执行。
————《Java并发编程的艺术》 方腾飞 魏鹏 程晓明 著