线程池的讲解

概述

现在在实现异步时,基本都是使用线程池来实现,线程池在工作应用的还是比较频繁的,本文将就线程池的使用、相关原理和主要方法源码进行深入学习。

线程池的基本使用

线程池的定义和优点

线程池,从字面含义来看,是指管理一组同构工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

“在线程池中执行任务”比“为每个线程分配一个任务”优势更多。通过重用现有的线程而不是创建线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。通过适当的调整线程池的大小,可以创建足够的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。

线程池的工作流程

线程的处理流程如下图所示。

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

  1. 默认情况下,创建完线程池后并不会立即创建线程, 而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
  2. 当线程数小于核心线程数时,每提交一个任务就创建一个线程来执行,即使当前有线程处于空闲状态,直到当前线程数达到核心线程数。
  3. 当前线程数达到核心线程数时,如果这个时候还提交任务,这些任务会被放到工作队列里,等到线程处理完了手头的任务后,会来工作队列中取任务处理。
  4. 当前线程数达到核心线程数并且工作队列也满了,如果这个时候还提交任务,则会继续创建线程来处理,直到线程数达到最大线程数。
  5. 当前线程数达到最大线程数并且队列也满了,如果这个时候还提交任务,则会触发饱和策略。
  6. 如果某个线程的控线时间超过了keepAliveTime,那么将被标记为可回收的,并且当前线程池的当前大小超过了核心线程数时,这个线程将被终止。

工作队列

如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将累积起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。常见的工作队列有以下几种,前三种用的最多。

  1. ArrayBlockingQueue:列表形式的工作队列,必须要有初始队列大小,有界队列,先进先出。
  2. LinkedBlockingQueue:链表形式的工作队列,可以选择设置初始队列大小,有界/无界队列,先进先出。
  3. SynchronousQueue:SynchronousQueue不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入SynchronousQueue中, 必须有另一个线程正在等待接受这个元素. 如果没有线程等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建 一个线程, 否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交 给执行它的线程,而不是被首先放在队列中, 然后由工作者线程从队列中提取任务. 只有当线程池是无解的或者可以拒绝任务时,SynchronousQueue才有实际价值.
  4. PriorityBlockingQueue:优先级队列,有界队列,根据优先级来安排任务,任务的优先级是通过自然顺序或Comparator(如果任务实现了Comparator)来定义的。
  5. DelayedWorkQueue:延迟的工作队列,无界队列。

饱和策略(拒绝策略)

当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略)。饱和策略有以下四种,一般使用默认的AbortPolicy。

  1. AbortPolicy:中止策略。默认的饱和策略,抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
  2. DiscardPolicy:抛弃策略。当新提交的任务无法保存到队列中等待执行时,该策略会悄悄抛弃该任务。
  3. DiscardOldestPolicy:抛弃最旧的策略。当新提交的任务无法保存到队列中等待执行时,则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”策略和优先级队列放在一起使用)。
  4. CallerRunsPolicy:调用者运行策略。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(调用线程池执行任务的主线程),从而降低新任务的流程。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。当线程池的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行(调用线程池执行任务的主线程)。由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得工作者线程有时间来处理完正在执行的任务。在这期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列中。如果持续过载,那么TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器过载后,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。

线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。Executors提供的线程工厂有两种,一般使用默认的,当然如果有特殊需求,也可以自己定制。

  1. DefaultThreadFactory:默认线程工厂,创建一个新的、非守护的线程,并且不包含特殊的配置信息。
  2. PrivilegedThreadFactory:通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限、 AccessControlContext、ContextClassLoader。如果不使用privilegedThreadFactory, 线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限。
  3. 自定义线程工厂:可以自己实现ThreadFactory接口来定制自己的线程工厂方法。

ThreadPoolExecutor源码解析

几个点

了解这几个点,有助于你阅读下面的源码解释。

  1. 下面的源码解读中提到的运行状态就是runState,有效的线程数就是workerCount,内容比较多,所以可能两种写法都用到。
  2. 运行状态的一些定义:RUNNING:接受新任务并处理排队任务; SHUTDOWN:不接受新任务,但处理排队任务; STOP:不接受新任务,不处理排队任务,并中断正在进行的任务;TIDYING:所有任务已经终止,workerCount为零,线程转换到状态TIDYING将运行terminate()钩子方法;TERMINATED:terminated()已经完成,该方法执行完毕代表线程池已经完全终止。
  3. 运行状态之间并不是随意转换的,大多数状态都只能由固定的状态转换而来,转换关系见第4点~第8点。
  4. RUNNING - > SHUTDOWN:在调用shutdown()时,可能隐含在finalize()。
  5. (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()。
  6. SHUTDOWN - > TIDYING:当队列和线程池都是空的时。
  7. STOP - > TIDYING:当线程池为空时。
  8. TIDYING - > TERMINATED:当terminate()方法完成时。

基础属性(很重要)

execute方法

使用线程池的submit方法提交任务时,会走到该方法,该方法也是线程池最重要的方法。

该方法就是对应上文的线程池的工作流程。主要调用到的方法为addWorker(见下文addWorker方法解读)。

addWorker方法

该方法主要目的就是使用入参中的firstTask和当前线程添加一个Worker,前面的for循环主要是对当前线程池的运行状态和有效的线程数进行一些校验,校验逻辑比较绕,可以参考注释进行理解。该方法涉及到的其他方法有addWorkerFailed(见下文addWorkerFailed源码解读);还有就是Worker的线程启动时,会调用Worker里的run方法,执行runWorker(this)方法(见下文runWorker源码解读)。

addWorkerFailed方法

该方法很简单,就是移除入参中的Worker并将workerCount-1,最后调用tryTerminate尝试终止线程池,tryTerminate见下文对应方法源码解读。

runWorker方法

上文addWorker方法里说道,当Worker里的线程启动时,就会调用该方法。

该方法为Worker线程开始执行任务,首先执行当初创建Worker时的初始任务,接着从工作队列中获取任务执行。主要涉及两个方法:获取任务的方法getTask(见下文getTask源码解读)和执行Worker退出的方法processWorkerExit(见下文processWorkerExit源码解读)。注:processWorkerExit在处理正常Worker退出时,没有对workerCount-1,而是在getTask方法中进行workerCount-1。

getTask方法

Worker从工作队列获取任务,如果allowCoreThreadTimeOut为false并且 workerCount<=corePoolSize,则这些核心线程永远存活,并且一直在尝试获取工作队列的任务;否则,线程会有超时时间(keepAliveTime),当在keepAliveTime时间内获取不到任务,该线程的Worker会被移除。

Worker移除的过程:getTask方法返回null,导致runWorker方法中跳出while循环,调用processWorkerExit方法将Worker移除。注意:在返回null的之前,已经将workerCount-1,因此在processWorkerExit中,completedAbruptly=false的情况(即正常超时退出)不需要再将workerCount-1。

processWorkerExit方法

该方法就是执行Worker的退出:统计完成的任务数,将Worker移除,并尝试终止线程池,最后根据情况决定是否创建一个新的Worker。两种情况下会创建一个新的Worker:1)被移除的Worker是由于异常而死亡;2)被移除的Worker是最后一个Worker,但是工作队列还有任务。completedAbruptly=false时,没有将workerCount-1是因为已经在getTask方法中将workerCount-1。

tryTerminate方法

该方法用来尝试终止线程池,主要在移除Worker后会调用此方法。首先进行一些状态的校验,如果通过校验,则在加锁的条件下,使用CAS将运行状态设为TERMINATED,有效线程数设为0。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
线程池是一种常见的并发编程技术,可以提高程序的性能和响应速度。它通过创建一定数量的线程,并将任务分配给这些线程来执行,从而减少线程的创建和销毁所带来的开销,提高系统的效率和稳定性。 线程池的基本构成包括任务队列、工作线程池和管理器。任务队列用于存储待执行的任务,工作线程池用于执行任务,管理器用于管理线程池的状态和任务分配。 在线程池启动时,会创建一定数量的工作线程,并将它们放入空闲队列中。当有任务提交时,管理器将任务添加到任务队列中,空闲线程从队列中取出任务并执行。当所有的线程都在执行任务时,新的任务将被暂存到任务队列中,等待空闲线程的出现。 线程池的优点是可以避免线程创建和销毁的开销,提高应用程序的性能和响应速度;可以控制线程的数量和执行状态,避免线程过多或过少所带来的问题;可以通过合理的任务分配和调度,提高系统的效率和稳定性。 线程池的缺点是需要占用一定的系统资源,包括内存和CPU资源;需要对任务的执行时间和线程的数量进行合理的配置,否则可能会导致系统的瓶颈和性能下降;需要对任务队列的大小和任务的优先级进行合理的设置,否则可能会导致任务执行的不公平和延迟。 总之,线程池是一种常见的并发编程技术,可以提高系统的效率和稳定性,但需要合理的配置和设计,才能发挥其最大的优势。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值