jdk原生的线程池模型主要由上述5个核心抽象组成。
1.Executor
执行提交的 Runnable 任务的对象,接口定义十分简单。
设计此接口的初衷 - 提供一种将任务提交与每个任务将如何运行的机制解耦的方法,包括线程使用、调度等的细节。通常使用 Executor 而不是显式创建线程。
2.ExecutorService
作为Executor的扩展,提供了管理终止的方法和可以生成 Future 以跟踪一个或多个异步任务的进度的方法。
可以关闭 ExecutorService,这将导致它拒绝新任务。提供了两种不同的方法来关闭 ExecutorService。 shutdown 方法将允许先前提交的任务在终止之前执行,而 shutdownNow 方法阻止等待的任务启动并尝试停止当前正在执行的任务。终止后,执行者没有正在执行的任务,没有等待执行的任务,也没有新的任务可以提交。应关闭未使用的ExecutorService 以允许回收其资源。
在Executor框架中引入了生命周期管理的概念。Executor新建和运行都是自然而然的,但是结束并不是。
问题:
- 如果在方法内部使用了Executor框架,那么其所关联的所有“资源”会随着方法结束而满足垃圾回收的条件吗?【不会。线程池对象的线程可能还在执行,自身也存在栈帧。】
- shutdown/shutdownNow的区别是什么?【在队列中的Task是否会再次执行,两者均无法管理已经在执行的任务了,会interrupt,但是是否被响应无法预知。】
- Terminate的含义是什么?【shutdown之后,所有的task都处理完成了。从这一点来看,只能是shutdown,不能是shutdownNow。】
- submit的含义是什么?【Executor的execute方法没有返回值,submit在执行的基础上,提供了Future对象返回的能力。】
- invokeAll的应用场景是什么?【批量提交任务,没啥特殊的】
AbstractExecutorService
提供 ExecutorService 执行方法的默认实现。
该类使用 newTaskFor 返回的 RunnableFuture 实现了 submit、invokeAny 和 invokeAll 方法,默认为本包中提供的 FutureTask 类。
例如,submit(Runnable) 的实现创建了一个关联的 RunnableFuture,该 RunnableFuture 被执行并返回。子类可以覆盖 newTaskFor 方法以返回除 FutureTask 之外的 RunnableFuture 实现。
Executors
Executors 类为此包中提供的执行器服务提供工厂方法。作为工厂类存在。
ThreadPoolExecutor
【重点】最核心的实现逻辑。
线程池解决了两个不同的问题:性能 资源管理
- 在执行大量异步任务时提供改进的性能,原因是减少了每个任务的调用开销(复用线程,节省创建线程的开销);
- 并且它们提供了一种限制和管理资源的方法。
每个 ThreadPoolExecutor 还维护一些基本的统计信息,例如完成任务的数量。
为了在广泛的上下文中有用,这个类提供了许多可调整的参数和可扩展性挂钩。Executors 工厂方法本身也提供了适配不同场景的内置工具:
- Executors.newCachedThreadPool(无界线程池,具有自动线程回收功能)
- Executors.newFixedThreadPool(固定大小的线程池)
- Executors.newSingleThreadExecutor(单个后台线程)
但出于安全性考虑,实践过程中建议我们自定义线程池工具,在手动配置和调整此类时使用以下指南:
corePoolSize & maximumPoolSize
ThreadPoolExecutor 将根据 corePoolSize(请参阅 getCorePoolSize)和 maximumPoolSize(请参阅 getMaximumPoolSize)设置的边界自动调整池大小(请参阅 getPoolSize)。当在方法 execute(Runnable) 中提交新任务时,如果运行的线程少于 corePoolSize,则创建一个新线程来处理请求,即使其他工作线程处于空闲状态。否则,如果少于 maximumPoolSize 线程正在运行,则仅当队列已满时才会创建一个新线程来处理请求。通过将 corePoolSize 和 maximumPoolSize 设置为相同,您可以创建一个固定大小的线程池。通过将 maximumPoolSize 设置为基本上无界的值,例如 Integer.MAX_VALUE,您允许池容纳任意数量的并发任务。最典型的是,核心和最大池大小仅在构造时设置,但它们也可以使用 setCorePoolSize 和 setMaximumPoolSize 动态更改。
【无条件提升线程数目至核心线程数】 → 【达到核心线程数后,任务入队列】 → 【队列满可以继续添加线程至最大线程数】
问题:
任务入队列是否需要判断是否存在可用线程?
On-demand construction
默认情况下,即使是核心线程也只会在新任务到达时才最初创建和启动,但这可以使用方法 prestartCoreThread 或 prestartAllCoreThreads 动态覆盖。如果您使用非空队列构造池,您可能希望预启动线程。
Creating new threads
使用 ThreadFactory 创建新线程。如果没有另外指定,则使用 Executors.defaultThreadFactory,它创建的线程都在同一个 ThreadGroup 中,并且具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,您可以更改线程的名称、线程组、优先级、守护程序状态等。如果 ThreadFactory 在从 newThread 返回 null 时未能创建线程,则执行程序将继续,但可能无法执行任何任务。线程应该拥有“modifyThread”RuntimePermission。如果使用池的工作线程或其他线程不具备此权限,则服务可能会降级:配置更改可能无法及时生效,关闭池可能会保持可终止但未完成的状态。(此处指代的是线程应该响应中断,否则shutdown机制本质上不会完美生效)
Keep-alive times
如果池当前有多于 corePoolSize 范围的线程,多余的线程如果空闲时间超过 keepAliveTime(请参阅 getKeepAliveTime(TimeUnit)),将被终止。这提供了一种在池没有被积极使用时减少资源消耗的方法。如果池稍后变得更加活跃,则将构造新线程。也可以使用方法 setKeepAliveTime(long, TimeUnit) 动态更改此参数。使用 Long.MAX_VALUE TimeUnit.NANOSECONDS 的值可以有效地禁止空闲线程在关闭之前终止。默认情况下,keep-alive 策略仅适用于 corePoolSize 线程以上,但方法 allowCoreThreadTimeOut(boolean) 也可用于将此超时策略应用到核心线程,只要 keepAliveTime 值非零。
Queuing
任何 BlockingQueue 都可以用来传输和保存提交的任务。此队列的使用与池大小相关: 如果运行的线程少于 corePoolSize,则 Executor 总是更喜欢添加新线程而不是排队。 如果 corePoolSize 或更多线程正在运行,Executor 总是更喜欢排队请求而不是添加新线程。 如果请求无法排队,则会创建一个新线程,除非这将超过 maximumPoolSize,在这种情况下,该任务将被拒绝。
排队的一般策略有以下三种:
直接交接。工作队列的一个很好的默认选择是 SynchronousQueue,它将任务交给线程而不用其他方式保留它们。在这里,如果没有立即可用的线程来运行任务,则尝试将任务排队将失败,因此将构造一个新线程。在处理可能具有内部依赖关系的请求集时,此策略可避免锁定。直接切换通常需要无限的 maximumPoolSizes 以避免拒绝新提交的任务。这反过来又承认了当命令的平均到达速度快于它们的处理速度时,线程无限增长的可能性。
无界队列。当所有 corePoolSize 线程都忙时,使用无界队列(例如没有预定义容量的 LinkedBlockingQueue)将导致新任务在队列中等待。因此,不会创建超过 corePoolSize 个线程。 (因此,maximumPoolSize 的值没有任何影响。)当每个任务完全独立于其他任务时,这可能是合适的,因此任务不会影响彼此的执行;例如,在网页服务器中。虽然这种排队方式在平滑请求的瞬时突发方面很有用,但它承认当命令平均到达速度快于处理速度时,工作队列可能会无限增长。
有界队列。有界队列(例如,ArrayBlockingQueue)在与有限的 maximumPoolSizes 一起使用时有助于防止资源耗尽,但可能更难以调整和控制。队列大小和最大池大小可以相互权衡:使用大队列和小池可以最大限度地减少 CPU 使用率、操作系统资源和上下文切换开销,但可能会导致人为地降低吞吐量。如果任务经常阻塞(例如,如果它们受 I/O 限制),系统可能能够为比您允许的更多线程安排时间。使用小队列通常需要更大的池大小,这会使 CPU 更忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。
Rejected tasks
当 Executor 关闭时,以及 Executor 对最大线程和工作队列容量都使用有限的界限,并且已经饱和时,在方法 execute(Runnable) 中提交的新任务将被拒绝。在任何一种情况下,execute 方法都会调用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor) 方法。提供了四个预定义的处理程序策略:
- ThreadPoolExecutor.AbortPolicy【默认】,处理程序在拒绝时抛出运行时 RejectedExecutionException。
- ThreadPoolExecutor.CallerRunsPolicy,调用 execute 本身的线程运行任务。这提供了一种简单的反馈控制机制,可以减慢提交新任务的速度。
- ThreadPoolExecutor.DiscardPolicy,简单地丢弃了无法执行的任务。
- ThreadPoolExecutor.DiscardOldestPolicy,如果 executor 没有关闭,则丢弃工作队列头部的任务,然后重试执行(可能再次失败,导致重复此操作。)
可以定义和使用其他类型的 RejectedExecutionHandler 类。这样做需要小心谨慎,尤其是当策略设计为仅在特定容量或排队策略下工作时。
Hook methods
此类提供protected作用域的可覆盖 beforeExecute(Thread, Runnable) 和 afterExecute(Runnable, Throwable) 方法,这些方法在执行每个任务之前和之后调用。这些可用于操纵执行环境;例如,重新初始化 ThreadLocals、收集统计信息或添加日志条目。此外,可以重写方法终止以执行任何特殊处理,一旦 Executor 完全终止需要完成。 如果钩子、回调或 BlockingQueue 方法抛出异常,内部工作线程可能依次失败、突然终止,并可能被替换。
Queue maintenance
方法 getQueue() 允许访问工作队列(任务队列)以进行监视和调试。强烈建议不要将此方法用于任何其他目的。当大量排队的任务被取消时,提供的两个方法 remove(Runnable) 和 purge 可用于协助存储回收。
Reclamation
程序中不再引用并且没有剩余线程的池可以被回收(垃圾收集)而无需显式关闭。您可以通过设置适当的保持活动时间、使用零核心线程的下限和/或设置 allowCoreThreadTimeOut(boolean) 来配置池以允许所有未使用的线程最终死亡。
Worker
Worker 主要维护运行任务线程的中断控制状态,以及其他次要的簿记。此类巧妙地扩展 AbstractQueuedSynchronizer 以简化获取和释放任务执行的锁。这可以在处理中断请求时,中断的是等待任务的工作线程而不是中断正在运行的任务的线程(符合我们对shutdown机制的理解)。我们实现了一个简单的不可重入互斥锁而不是使用 ReentrantLock,因为我们不希望工作任务在调用 setCorePoolSize 等池控制方法时能够重新获取锁。此外,为了在线程真正开始运行任务之前抑制中断,我们将锁定状态初始化为负值,并在启动时将其清除(在 runWorker 中)。
ctl
线程池的主控制状态ctl是一个原子整数,包装了两个概念字段(1-工作线程数量workerCount;2-线程池所处的生命周期阶段runState)。
- workerCount 表示有效线程数;
- runState 表示是否正在运行、正在关闭等
为了将它们打包成一个int,我们限制workerCount为(2^29 )-1(约 5 亿)个线程,而不是 (2^31)-1(20 亿)个其他可表示的线程。如果这在未来成为问题,可以将变量更改为 AtomicLong,并调整下面的移位/掩码常量。但是在需要之前,这段代码使用 int 会更快更简单一些。作者通过巧妙的设计将整型变量的二进制形式拆分为两部分。
workerCount 是允许启动和不允许停止的工作程序的数量。该值可能与实际的活动线程数暂时不同,例如,当 ThreadFactory 在被询问时未能创建线程时,以及退出线程在终止前仍在执行簿记时。用户可见的线程池大小为workers(HashSet<Worker>)的当前大小。
runState 提供主要的生命周期控制,取值:
- RUNNING:接受新任务并处理排队任务
- SHUTDOWN:不接受新任务,但处理排队任务
- STOP:不接受新任务,不处理排队任务,并中断正在进行的任务
- TIDYING:所有任务都已终止,workerCount 为零,转换到状态 TIDYING 的线程将运行 terminate() 钩子方法. runState 随着时间的推移单调增加,但不需要达到每个状态。
可能的状态转换:
- RUNNING -> SHUTDOWN 调用 shutdown()
- RUNNING 或 SHUTDOWN -> STOP 调用 shutdownNow()
- SHUTDOWN -> TIDYING 当队列和池都为空时
- STOP -> TIDYING 当池为空时
- TIDYING -> TERMINATED 当 terminate() 钩子方法完成时,在 awaitTermination() 中等待的线程将在状态达到 TERMINATED 时返回。
检测从 SHUTDOWN 到 TIDYING 的转换并不像你想的那么简单,因为队列可能在非空后变为空,在 SHUTDOWN 状态下反之亦然,但我们只能在看到它为空之后终止,我们看到 workerCount为 0(有时需要重新检查——见下文)。
workQueue
用于保存任务并移交给工作线程的队列。我们并不要求 workQueue.poll() 返回 null 一定意味着 workQueue.isEmpty(),所以完全依赖 isEmpty 来查看队列是否为空(例如,我们必须这样做,在决定是否从 SHUTDOWN 过渡到 TIDYING 时) .这适用于特殊用途的队列,例如 DelayQueues,其中 poll() 允许返回 null,即使它稍后可能在延迟到期时返回非 null。
mainLock
锁定访问 workers 和相关簿记。虽然我们可以使用某种并发集合,但事实证明通常最好使用锁。
其中一个原因是可以串行化操作interrupt Idle Workers,这避免了不必要的中断风暴,尤其是在 shutdown 期间。否则退出线程将同时中断那些尚未中断的线程。它还简化了一些相关的最大池大小等统计簿记。我们还在shutdown和shutdownNow上持有mainLock,以确保 workers 是稳定的,同时分别检查中断和实际中断的权限。
workers
包含池中所有工作线程的集合。仅在持有 mainLock 时访问。
termination
等待条件(基于锁的机制)以支持 awaitTermination
largestPoolSize
跟踪获得的最大池大小。只能在 mainLock 下访问。
completedTaskCount
已完成任务的计数器。仅在工作线程终止时更新。只能在 mainLock 下访问。
threadFactory
新线程的工厂。所有线程都是使用这个工厂创建的(通过方法 addWorker)。所有调用者都必须为 addWorker 失败做好准备,这可能反映了系统或用户限制线程数的策略。即使它不被视为错误,创建线程失败也可能导致新任务被拒绝或现有任务卡在队列中。即使在尝试创建线程时可能会抛出诸如 OutOfMemoryError 之类的错误时,我们也会更进一步并保留池不变量。由于需要在 Thread.start 中分配本机堆栈,因此此类错误相当普遍,并且用户将希望执行干净的池关闭以进行清理。可能有足够的内存可供清理代码完成,而不会遇到另一个 OutOfMemoryError。
handler & defaultHandler
线程池饱和或关闭时的处理器。
keepAliveTime
等待工作的空闲线程超时(以纳秒为单位)。当存在多于 corePoolSize 或 allowCoreThreadTimeOut 时,线程使用此超时。否则,他们将永远等待新的工作。
allowCoreThreadTimeOut
如果为 false(默认),核心线程即使在空闲时也保持活动状态。如果为真,核心线程使用 keepAliveTime 超时等待工作。
corePoolSize
核心池大小是保持活动的最小工人数(并且不允许超时等),除非设置了 allowCoreThreadTimeOut,在这种情况下最小值为零。由于工作人员计数实际上存储在 COUNT_BITS 位中,因此有效限制为 corePoolSize & COUNT_MASK。
maximumPoolSize
最大池大小。由于工作人员计数实际上存储在 COUNT_BITS 位中,因此有效限制为 maximumPoolSize & COUNT_MASK。
shutdownPerm
shutdown 和 shutdownNow 的调用者所需的权限。我们还要求(参见 checkShutdownAccess)调用者有权实际中断工作集中的线程(由 Thread.interrupt 管理,它依赖于 ThreadGroup.checkAccess,而后者又依赖于 SecurityManager.checkAccess)。仅当这些检查通过时才会尝试关闭。 Thread.interrupt 的所有实际调用(请参阅 interruptIdleWorkers 和 interruptWorkers)都会忽略 SecurityExceptions,这意味着尝试的中断会静默失败。在关闭的情况下,它们不应该失败,除非 SecurityManager 有不一致的策略,有时允许访问线程,有时不允许。在这种情况下,未能真正中断线程可能会禁用或延迟完全终止。 interruptIdleWorkers 的其他用途是建议性的,实际中断失败只会延迟对配置更改的响应,因此不会进行异常处理。
内部类
Worker
核心问题1:线程池的线程如何复用?Java的Thread类机制(多次启动一个线程是不合法的。特别是,线程一旦完成执行就可能不会重新启动)
Worker类的定义比较有趣,持有Thread类的同时,实现了Runnable接口,并且与Thread实现相互依赖关系。
答案:线程池并没有打破Java的Thread类机制,而是在Worker类的run方法中引入“无限循环”,Worker类作为ThreadPoolExecutor的内部类,将自己的run方法委托给外部累的runWorker方法执行,因此可以通过对runWorker的分析可以明确线程池对线程复用的处理。此处对处理task的来源有两处:worker本身的firstTask,getTask方法获取 workQueue 中存储的task。其中特别值得注意的是 getTask 内部存在对线程idle时间的的处理逻辑,通过BlockingQueue的poll(long timeout, TimeUnit unit)方法,巧妙的处理了idle超时场景。
核心问题2:Worker继承AQS的作用是什么?
答案在worker解析中已经有所解释,本质上是为了简化锁的实现。为何要将Worker设计为一个锁?因为每个Worker本质上都是一个线程在执行其逻辑,对线程池本身的状态,eg. ctl的处理是需要加锁的。