一、序言
本文介绍线程池的思想以及线程池的工作流程。
二、银行的工作模式
日常生活中,我们或多或少都接触过银行。那么银行的工作模式是怎样的呢?我们通过上图来看看这个流程。
- 当客户前去银行办理业务时,他们首先会到接待区也就是前台取号
- 取到号之后通常人们会到等待区等待自己被叫号
- 如果自己被叫号,那么就去对应窗口处理业务
- 自己的业务办理完成离去之后,窗口的业务人员会继续叫下一个人的号处理业务
- 如此循环往复,直至下班或者没有客户需要办理业务
这个流程是比较简单的。但我们发现似乎银行的业务窗口并不是全部都有人在,这是为什么?试想一下,若银行当前只有一人或几人需要办理业务,可能一两个窗口便能处理了,此时若将全部业务窗口开放岂不是在白白浪费人力资源?若将所有窗口划分为常驻窗口(一直有业务员守着)和流动窗口(当客流量比较大时才有业务员值守),这是不是就可以实现节约人力资源的目的了?
三、池化思想
在我们的程序中,线程也是一种比较宝贵的资源。我们是否能够借鉴银行的工作模式来使用线程呢?答案是肯定的。
我们将银行的业务柜台换成线程池(包含多个线程,例如:线程 A,线程 B…),接待区换成管理器(负责管理和处理流程),等待区换成工作队列(负责暂时存放任务),来看看程序是如何模拟银行工作的。
- 当业务请求到达管理器,管理器先检查线程池是否有空余线程。若有,那么交由空余线程执行
- 如果没有空余线程,管理器检查工作队列是否有剩余位置。若有,将请求暂时放入
- 如果工作队列已满,管理器将会给出一种处理方案(例如:让请求稍后重试,或者拒绝请求)
- 当线程池中的线程处理完一个请求任务之后,会自动的去工作队列取下一个任务进行处理
- 此流程直至工作队列没有任务,外部也没有请求才算结束
如果我们采取上面这种方案取使用线程,可以发现:
- 线程将是可控的(可以控制线程的数量,以及线程的创建和销毁时机)
- 线程是可复用的(线程处理完一个任务之后会自动的获取下一个任务进行处理)
上面的内容其实是一种线程池化思想。线程池化是为了优化线程的管理和利用,而采取的一种技术。它的基本思想是创建一组可重用的线程,这些线程在需要时被动态分配给任务,执行完任务后又可以返回线程池中等待下一次调用。线程池化思想主要用于解决线程频繁创建和销毁所带来的性能开销,以及更有效地利用系统资源的问题。
四、线程池的核心概念
线程池的核心概念一共包括三个:
- 线程池管理器(ThreadPool Manager):负责创建、管理和销毁线程池,以及分配任务给线程池中的线程。
- 工作队列(Work Queue):用于存放待执行的任务,线程池中的线程会从工作队列中获取任务进行执行。
- 线程池(Thread Pool):由一组预先创建的线程组成,它们可以重复利用,避免了频繁创建和销毁线程的开销。
五、线程池的几个问题
5.1 线程池中的线程如何分类
在上文中我们已经分析过银行的窗口应该分为常驻窗口和流动窗口。在线程池中,线程也应该分类。理由和银行的窗口是一样的。如果我们线程池中处理的任务量过小是没有必要开启这么多线程的。那么,我们在线程池中如何称呼这两种线程呢?我们将其称为核心线程(参考银行常驻窗口)和非核心线程(参考银行流动窗口)。
5.2 线程池中线程的创建时机
银行柜台的常驻窗口,在上班的时候业务人员需要提前坐在位置上准备接待客户。所以,银行一开始就将业务人员安排好了。我们在线程池中也可以采用这种方案,即在初始化阶段创建核心线程。这种方案在线程池创建之初就创建出了核心线程没什么好说的。
但是,初始化阶段创建线程会存在一些问题。例如:我们有 10 个核心线程,若一开始就创建好 10 个线程,但是只有 3 个任务需要处理,这是不是就造成了资源浪费。所以,线程池还可采用延迟创建核心线程的方案。
当第一个请求来到线程池,线程池发现核心线程数为 10,但是现在的实际线程数为 0,此时线程池就会创建一个线程。紧接着第二个请求来到线程池,线程池发现核心线程数为 10,实际线程数为 1,此时线程池就会创建下一个线程。以此类推直至达到核心线程数。核心线程数已经满了,此时若有任务来,那么就会放入到工作队列中。如果工作队列一直有空闲的位置存储任务,那么就没有非核心线程的事了。若工作队列满了,还有请求,那么线程池就会判定此时任务比较繁忙就会创建非核心线程。综上,可以发现延迟创建核心线程时:
- 核心线程是在第一次有任务请求时创建的,直至达到最大核心线程数
- 非核心线程是在工作队列已满的情况下创建的,直至达到最大非核心线程数
5.3 线程池应该采用哪种方案创建核心线程
- 在初始化阶段创建核心线程:在创建线程池时,可以通过设置核心线程数来指定在线程池初始化阶段就创建的核心线程数量。这些核心线程会立即创建,并且一直保持活动状态,等待任务的到来。这样可以减少任务提交时的线程创建开销,但也会增加线程池的初始开销。
- 延迟创建核心线程:在任务提交时才创建核心线程。在这种情况下,线程池在接收到任务时才会创建核心线程来执行任务。这样可以延迟线程的创建时间,降低线程池的初始开销,但也可能导致任务开始执行时的一些延迟。
无论是哪种方式,一旦核心线程被创建,它们就会一直保持活动状态,直到线程池被显式地关闭。因此,即使线程池中的核心线程在初始化阶段未被创建,它们也会在第一次任务提交时被创建,并且保持活动状态,直到线程池关闭。所以,我们接下来的讨论围绕延迟创建核心线程进行。第一种方案也是有业务使用场景的,所以没有最好,只有合不合适。
六、线程池的工作流程
- 线程池开启之后,首先检查当前是否有任务
- 若没有任务则进行等待
- 若有任务,则判断当前线程数是否小于核心线程数
- 若小于,则创建一个核心线程并处理当前请求
- 若已达最大核心线程数,线程池会检查工作队列是否已满
- 若工作队列未满,则将任务暂时添加到工作队列
- 若工作队列已满,则判断当前线程数是否大于最大线程数(最大线程数 = 核心线程数 + 非核心线程数)
- 若未达最大线程数,此时会创建一个非核心线程并处理当前请求
- 若已达最大线程数,线程池会执行拒绝策略(即拒绝接收任务或抛出异常等)
上面的流程中还有两个内容没有涉及到:
核心线程处理完一个任务之后,会继续到工作队列获取任务,如果获取到任务则继续执行,若没有获取到任务,则处于等待任务的状态(即核心线程被创建出来之后不会被释放)。
非核心线程处理完一个任务之后,会继续到工作队列获取任务,如果获取到任务则继续执行,若没有获取到任务,等待一段时间会被回收(非核心线程若没有使用,一段时间之后会被释放)。