线程池这一块实际上东西并并不是太多,主要在于理解java中线程池的几个参数,从而进一步理解线程池的原理,同时需要简单掌握java线程池的使用。
此外,在我看来,线程池的相关知识可以和Executor框架结合起来进行理解。
为什么需要线程池
首先需要讲明的一点,在HotSpot VM的线程模型中,java线程并不是我们在操作系统中所学到的线程,它并不是真正的线程。在该模型中,java线程被一对一映射成为本地操作系统。java线程启动时会创建一个本地操作系统线程;当该java线程终止时,该操作系统线程也会被回收。
然后需要可说的就是,为何需要线程池?线程的作用是什么?
这里的需要明白的前提有三:
- 首先,我们的java程序是处在一个并发的场景中的,所以需要多个线程
- 其次,我们线程创建与销毁是需要耗费时间与资源的。它的开销与实际的线程执行任务的开销相差并不大,所以这个开销不能忽略。
- 最后,线程需要被管理。线程数量不能无限制增加,因为总的资源就这么多,数量太大无法管理,空闲的线程需要被处理。
因此,java的线程池就诞生了,它能起到以下的作用
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达是,任务可以不需要等到线程创建就立即执行
- 提高线程的可管理性。线程是稀缺资源,若无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
大体上理解,线程池将已经创建的线程放到池中进行维护,当并发环境下,多个任务到达,此时,从线程中拿出线程进行任务执行。线程池是个容器,里面装的是线程,但同时,它不仅仅是个容器,它还具有管理功能,它能够管理线程,控制线程数量,控制线程的创建。
线程池的主要运行逻辑
上图便是线程池的主要处理流程,或者说是运行的逻辑。
下面我们便对此进行一个解释,方便理解。
先根据上图解释下几个概念
核心线程是什么鬼?
- 说到核心线程,那么就会有非核心线程,是的,线程池里的线程分为核心线程与非核心线程
- 二者的主要区别就在于销毁。当线程空闲时间超过限制,非核心线程就会被销毁,而核心线程不会被销毁。(通过allowCoreThreadTimeOut也可以让核心线程被销毁,此时二者基本一样)
- 所以,这里核心的含义可以理解为,该线程不能被销毁,它一定要存在,它的存在是为了应对突然到来的任务。它要保证线程池的基本业务能力。
- 我们可以这样理解,对于一个处理多个业务的公司,我们一定要有一批固定的员工,就算现在业务不是很多,我们也要养这些员工,因为它是我们公司的核心,一旦有业务,不会出现无人可做的情况。同时,这个核心员工的数量也是我们对于并发中任务数量的一个预期,我们公司的核心员工数量能够保证,在正常情况下,每个人都有事可做,且不会太累,而每个任务也能被及时的处理,这是一个平衡。当然,在特定时期,可能会出现业务量激增的情况,原本的核心员工就满足不了超额的业务量了,此时,我们就需要招一些临时工(非核心线程),它们具有和我们核心员工一样的能力,但是是临时工,它们被临时招进来处理这些超出来的业务。当特定时期度过,我们发现业务量恢复正常,公司也养不起这么多的员工,因此,我们将空闲的临时工一个个的辞退,从而使得公司正常运行。
- 核心线程主要与corePoolSize这个参数有关(java中提供的线程池的构造函数参数,后面会讲),当当前线程数少于corePoolSize时,新创建的线程就是核心线程,当大于时,新创建的线程就是非核心线程。
这里的队列又是什么东西?
- 清晰的看到,上面流程中,在核心线程池已满的情况下,会将当前的任务放到队列里
- 然后线程池中的线程(空闲)就会从队里中去获取任务来执行,使得自己不空闲(这里是要用到同步的,不然会乱)
- 所以是在核心线程池已满的情况下,将任务存储起来,由线程池来向任务队列发起主动获取
- 访问任务队列并获取到任务的线程可能是核心线程也可能是非核心线程
整体的流程叙述:
- 首先用户提交任务给线程池
- 线程池进行查询,看看核心线程池是否已满(即当前的线程数是否小于corePoolSize)。如果未满,则创建新的线程执行该任务(此时是核心线程),结束退出;如果满了,则进行下一步。
- 此时核心池满了,尝试将任务放到队列中去,等待线程来获取任务。此时也会有两种情况,队列满了,则直接创建新线程执行该任务(此时是非核心线程),进入下一步;队列未满,放入队列中,等待线程获取被执行,结束退出。
- 当队列已满,且总线程数(核心+非核心)达到了maximumPoolSize时,此时就会出现问题,则交由异常处理策略来进行处理。
总的流程便是如上所示,这里再针对性的讲几个问题。
首先,为什么当核心池未满时,逻辑是直接创建新线程来执行任务,而不是先判断核心池中是否有空闲的核心线程呢?
- 这个问题主要在于,我们一开始需要也指定了corePoolSize,这个数量的线程我们是期望达到并保持的。
- 如果一开始业务量不大,就创建了很少数量的核心线程,偶有空闲,所以我便不创建新的核心线程,那么此时如果忽然增大业务量呢,那就麻烦了,得马上新建。那么此时就和预期不一样了。
- 线程池的逻辑是要尽可能的保证线程数在corePoolSize这个水平,同时尽可能快的达到这个目标,所以只要来一个新任务,我发现当前线程数未满足预期,则马上新建,从而当我来了corePoolSize个任务时,我便达到了预期。这个过程也叫预热。
其次,为什么放到队里中去呢?
- 这个在上面也讲过,当核心池满,则不创建新的线程,将任务放到队列中
- 此时的逻辑是,由空闲的线程去获取任务,而不是由任务去找空闲的线程,从而将线程做为主动,整体的代码逻辑会更清晰
- 其次,整体的同步也只需要做队列就可以了,复杂度降低
然后是当队列满了为什么要创建非核心线程呢?
- 这里队列在我理解,其实是对于核心线程池能力的一种信任。
- 只要放在队列中,就代表,在一定时间,该任务一定能被我们的核心池线程执行到,所以不至于等待太久。
- 当队列满了,说明,再进去的任务,很就也不会被执行到,业务量太大了,此时不能光靠核心线程了,那么此时为了让任务的响应时间不至于太长,我们就临时创建非核心线程来执行,应对突然激增的业务量。
- 当然了,创建新的线程需要开销,所以这个算是在不得已的情况下才会创建新线程。
这里没有提到的是:
- 当业务量没有那么大,非核心线程会空闲,超过指定的时间,就会被销毁
- 核心线程不会被销毁
- 因为线程也要占用资源,所以不允许过多的线程存在,所以销毁非核心,而核心所占用的资源量我们可以接受,并且其可以应对预期的业务量。
总体来说,线程池的逻辑就是,先满足核心池的预期,进行预热;如果预热结束,则尽量依靠核心池的能力进行处理,尽量不创建新的线程;如果实在不行,再创建新的非核心线程来帮忙;一旦业务量不大了,我就销毁非核心线程,节省资源。
java中的线程池
ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务,它也是Executor框架的一个最重要与核心部分。
它在进行创建时的构造函数主要需要以下几个参数:
- corePoolSize
- maximumPoolSize
- keepAliveTime
- BlockingQueue
- TimeUnit
- ThreadFactory
- RejectedExecutionHandler
下面分别来进行介绍:
- corePoolSize,上面也提到过,核心线程的数量
- maximumPoolSize,总的最大线程数量
- keepAliveTime空闲线程的超时时间,超过改时间,非核心线程会被销毁
- BlockingQueue,用于存储任务的队列
- TimeUnit,空闲超时的时间单位,有ms,s等
- ThreadFactory,创建线程的工厂方法
- RejectedExecutionHandler,在当前线程数大于maximumPoolSize时的处理策略
由此,我们可以看到,ThreadPoolExecutor这个线程池,它暴露了那么多的参数给我们,于是我们便可以去定值我们想要的线程池,根据这些参数。
下面来看在java中,Executor框架的工具类提供给我们的几种类型的ThreadPoolExecutor。
FixedThreadPool
- 其被称为可重用固定线程数的线程池。
- 其主要参数为corePoolSize=maximumPoolSize=n,且使用的是无界队列,LinkedBlockingQueue,容量为Integer.MAX_VALUE
- 其逻辑如下
a 如果当前运行的线程数少于corePoolSize(n),则创建新线程来执行任务
b 在线程池完成预热之后,将任务加入LinkedBlockingQueue
c 线程执行完a中的任务后,会在循环中反复从队列中获取任务来执行- 使用无界队列对于线程池的影响:
1)当线程池中的线程数达到corePoolSize之后,新任务将在队列中进行等待,因为队列无界,所以永远不会创建非核心线程
2)maximumPoolSize成为了无效参数
3)keepAlived成为了无效参数
4)运行中的线程池不会拒绝任务
从这个列子中可以看到,不同的参数可以实现不同功能的线程池。
SingleThreadExecutor
- 其是一个使用单个worker线程的Executor
- 参数设定为corePoolSize=maximumPoolSize=1,且是哟经的是LinkedBlockingQueue作为无界队列
- 其逻辑就不细述了,明显同时只会有一个线程在运行,便于同步
CachedThreadPool
- 其是一个会根据需要而创建新线程的线程池
- 其参数设定为corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,队列是一个无容量的队列
- 首先,因为corePoolSize=0,且maximumPoolSize为无穷,所以会不断的创建新的非核心线程,这就满足了需要,只要有需求,我就创建新的线程来满足需求
- 其次,因为 队列是无容量的(synchronizeQueue),所以主线程提交了任务之后,就必须等待任务被取出,无法提交新任务,所以这一步其实就相当于任务的传递,手递手将任务递给空闲的线程,或者是新的线程
- 这两点结合之后,线程池的逻辑如下:
a 当主线程提交任务(offer()方法),当前线程池中有空闲的线程,正在执行poll(),则offer与poll匹配成功,主线程将任务交给空闲线程执行
b 当初始线程池为空,或者线程池中没有空闲的线程时,此时没有线程执行poll()方法,此时,步骤a失败,将创建一个新的线程执行任务
c 新创建的线程执行完成之后,会执行poll方法,然后空闲线程在队列中等待60s,如果这段时间没有任务提交,那么该线程就会被终止。- 所以最终,实现了这样一个线程池:线程数无限制,有空闲线程则使用空闲线程,否则创建新线程,一定程度上减少了频繁的创建与销毁,节省了开销
ScheduledThreadPoolExecutor
- 该类继承自ThreadPoolExecutor
- 主要用来在给定延迟之后运行任务或定期执行任务
- 其使用的是一个Delay队列,其中的元素是ScheduledFutureTask,元素中有time(将被执行的时间,实际执行时间应该小于等于这个),sequenceNumber(序号)period(间隔周期)这些字段
- Delay队列封装了一个优先队列,time小的在前面
- 当线程从队列中拿到任务,要保证time变量大于等于当前时间,
- 执行任务之后,如果没有执行完成,需要修改这个任务的time,然后放回队列中去
提交任务
向线程池提交任务有两种方法
- execute():提交一个Runnable接口的实例,不需要返回值
- submit():需要返回值,会返回一个Future对象,至于Future对象有什么用,那就之后再说了,这个很有用
参考资料
https://blog.csdn.net/lift_class/article/details/70216690
《java并发编程的艺术》