前言
线程和线程池是在日常开发和学习中难以避免的内容,自己也是经常看,但是过段时间就忘记一些,归结了两点原因:
对于一些概念和流程没有理解透彻,浅尝而止;
总结的笔记很散,没有一个地方去归纳汇总,有新的内容会新开一个文档去记录,导致内容很零散;
利用公司包装好的线程工具,没有深入了解其实现,停留在API层面
本系列文章是希望能够从头开始记录学习线程池的内容和过程,帮助自己更好更深入的去了解线程池,同时也想给后续新接触java和线程池的朋友一点学习思路,也希望能与工作多年的大佬们交流看法。
关于本系列的标题,本意是希望通过我们的成语去表达每期的内容方向,言简意赅。
由于工作关系,计划本系列会周更1到2两次(都在周末),也算是逐渐培养自己写文章记录作总结的习惯。
线程池
带着问题去了解
看源码或者想了解背后的实现,最好需要带着问题去看,有目的性的去看,这样能够有明显的收获感,比如阅读了一遍源码,解答了我的xx问题,这种带有问题的阅读能够带来更加深刻的认识和理解。如果只是为了了解下而去看,可能在一段时间后就忘记了。
因此,在真正进入了解线程池的运作之前,先罗列下自己想要了解的问题:
线程池是如何创建的?
线程池的核心线程与空闲线程是如何实现工作的?
线程池的拒绝策略是如何实现和工作的?
...
正文
本期,想让我们了解下线程池是如何创建并使用的,这个过程中需要用到什么?
在最开始接触线程池的时候,会被Executor,ExecutorService,ThreadPoolExecutor,Executors几个类弄混,搞不清谁是谁,谁的作用是这个,谁的作用是哪个,下面先用一张类图说明前三者的关系

Executor:作为最基础的接口定义了一个方法execute(Runnable command),用于执行给定的Runnable任务。并且它是最基本的线程执行模型,但并不提供额外的管理功能,如线程终止、任务提交状态检查等。
ExecutorService:作为Executor的一个子类,在其基础上提供了更多的方法扩展,例如线程池关闭,中断等。
ThreadPoolExecutor:作为ExecutorService的具体实现,提供了高度自定义线程池自定义的能力(这也是我们在日常开发中用到的最多的类,根据不同的业务情况去创建不同的线程池),是构建线程池的首选类。
Executors:是juc中的一个工具类,里面提供了一些预制好的线程池以供使用(预定义的线程池也是使用ThreadPoolExecutor创建的)
那么,既然juc中提供的工具类预先定义好的线程都是利用ThreadPoolExecutor去创建的,那么重点自然是他。
ThreadPoolExecutor
ThreadPoolExecutor是ExecutorService的主要实现,专门用于创建和管理线程池,提供了强大的线程池管理能力,允许开发者根据应用需求精细地控制线程池的行为。
创建对象的方式是什么?对,通过构造函数,那么首先来看下ThreadPoolExecutor的构造函数
借用IDEA的Structure功能可以清晰的看到在
ThreadPoolExecutor类中有四个抽象方法,从上至下分别补充了默认的线程工厂,拒绝策略,下面先看一下最完整的构造函数的样子是什么
可以看到最完整的构造函数一共有7个参数,分别是
corePoolSize:核心线程数。这是线程池中的最小线程数量,即使没有任务执行,线程池也会维持这个数量的线程。
maximumPoolSize:最大线程数。线程池允许创建的最大线程数量。当队列满且请求的任务数量超过 corePoolSize 时,线程池会创建新线程直到达到这个上限。
workQueue:工作队列。这是一个阻塞队列,用于存放等待执行的任务。当线程池中的线程数达到 corePoolSize 后,新来的任务会被放入这个队列中等待执行。
keepAliveTime:空闲线程存活时间。当线程池中的线程数超过 corePoolSize 时,多余的空闲线程会在指定的时间后被终止。这个时间就是 keepAliveTime。
unit:时间单位。keepAliveTime 的单位,如秒、毫秒等。在代码片段中未直接出现,但在 unit.toNanos(keepAliveTime) 调用中转换为纳秒单位存储。
threadFactory:线程工厂。用于创建新线程的对象,可以自定义线程属性,如线程名称、优先级等。
handler:拒绝策略。当线程池无法处理更多任务(即所有工作线程和队列都已满)时,将采取的拒绝策略,如丢弃任务、终止线程、调用者等待等。
拒绝策略又可以分为以下几种
AbortPolicy:这是默认的拒绝策略,当线程池无法接受新任务时,它会抛出RejectedExecutionException异常,导致调用execute()方法的线程失败。
CallerRunsPolicy:在这种策略下,当线程池无法接受新任务时,提交任务的线程会执行该任务。这意味着调用者线程将直接运行任务,而不是将其放入线程池。
DiscardOldestPolicy:在这种策略下,当线程池无法接受新任务时,此策略会从队列中移除最早的未处理任务,然后尝试再次提交当前任务。这样,较旧的任务被牺牲,以便为新的任务腾出空间。
DiscardPolicy:在这种策略下,会丢弃无法处理的任务,不会抛出异常,也不会通知调用者。
为了方便记忆,可以对以上分别起个名字来帮助我们加深理解和记忆,笔者起的名字分别为
AbortPolicy:及时止损
CallerRunsPolicy:责有所归
DiscardOldestPolicy:以旧换新
DiscardPolicy:默默无闻
什么??? 拒绝策略竟然还有两个额外的实现,定睛一看,原来是org.apache.juli包引入的,究其根本,这两种策略类型并不是java标准库中定义的拒绝策略,具体的含义为
DropLastPolicy:这个策略可能意味着当线程池无法接受新任务时,它会丢弃最近添加到队列中的任
务。这与之前提到的DiscardOldestPolicy拒绝策略相反,后者会丢弃队列中最老的任务。
DropFlushPolicy:
这个策略不太像之前几种比较直观。
看了下大概得源码,意思是当提交的任务被拒绝时,会继续尝试将该任务添加到任务队列中,如果添加
成功,那么停止这一行为,否则会检查线程池是否关闭,如果关闭也停止这一行为,如果线程池没有关
闭,那么会持续进行以上步骤,直到添加成功,或者达到停止这一行为的条件
同样的,我们也起一个别名来对以上两种拒绝策略加以描述
DropLastPolicy:喜新厌旧
DropFlushPolicy:锲而不舍
而以上的核心线程,最大线程,任务队列等本次就不做多余的阐述了,后续会一一介绍。下面先着重讲下threadFactory也就是线程工厂,这也是本人在最初学习线程池时没搞太懂的地方。
到这里为止,简单介绍了ThreadPoolExecutor是什么以及有哪些参数组成
ThreadFactory
线程工厂(ThreadFactory)是一个接口,接口内部定义了一个方法,如下图所示
该方法的入参是一个Runable对象,出参是一个线程实例。当我们创建线程池时,就需要选择一个线程工厂,例如上述提到了ThreadPoolExecutor的构造函数中,使用的是默认线程工厂,位于Executors类中,全路径为java.util.concurrent.Executors.DefaultThreadFactory,当线程池有新的任务提交,并且需要新建线程时,线程工厂就开始发挥作用了,那就是创建线程,这个线程最终也会返回给线程池供其使用。
因此,线程工厂的主要作用是负责创建和管理线程池中的线程,提供了以下功能
线程命名:给线程命名,以区分不同场景的线程,便于调试和分析;
设置线程优先级和守护状态:设定线程的优先级和是否为守护线程;
线程组管理:将线程分配到不同的线程组,有助于组织和管理线程;
异常处理:在创建线程时可以捕捉异常,从而进行下一步的动作;
资源优化:可以在创建线程时调整线程栈大小,以减小内存消耗等;
自定义:可以实现ThreadFactory接口,根据特定的业务场景来实现更为复杂的动作,例如可以在线程
创建的前后做一些监控记录,从而更好的观察线程运行的状态;
那么,问题来了,线程工厂具体是怎么工作的呢?这块就需要简单看下源码了
当我们在执行线程池的execute方法时,会提交一个任务,线程池内部会来判断应该使用核心线程还是新建线程,还是触发拒绝策略
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
上述是execute执行的方法内部,我们可以看到在经过各种判断后最终做的就是两件事,一个是addWorker,一个就是reject,从名字上看,添加线程的工作肯定就是前者了。
而worker又是什么呢?worker就是要添加的任务。addWorker方法内部的代码比较多并且稍显复杂,我们只关注在这个过程中,线程工厂干了什么
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get();;) {
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask); --- here!!! 这里哇
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
...后续代码待后续讲解
可以发现,有这么一行代码,w = new Worker(firstTask),这里调用了Worker的构造方法,构造方法能干什么?没错,你说对了,初始化对象!,让我们在走进去构造方法的内部
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
哎,这里就可以看到调用了getThreadFactory().newThread(this);方法完成了线程的创建
当然这里只是大概走了下线程池的工作流程,其内部的逻辑远远不止于此,我也会在后续的文章里抽丝剥茧,和大家一起学习线程池的内部流程,做到知其然知其所以然
结语
到这里,介绍了常见的Executor,ExecutorService,ThreadPoolExecutor,Executors概念;
紧接着介绍了线程池中的重中之重ThreadPoolExecutor,介绍了他的组成部分;
最后介绍了什么是线程工厂,并简单罗列了下源代码介绍了其是如何工作的。
本篇文章算是一个线程池的基本介绍,帮助大家更好的了解线程池,熟悉线程池,后续我们会一一探究其他内容。
故不积跬步,无以至千里
不积小流,无以成江海。
与君共勉!