Flash图解线程池 | 阿里巴巴面试官喜欢问的线程池究竟是什么?

前言

前几天小强去阿里巴巴面试Java岗,止步于二面。java

他和我诉苦本身被虐的多惨多惨,特别是深挖线程和线程池的时候,竟然被问到不知道如何做答。程序员

对于他的遭遇,结合他过了一面的那个嘚瑟样,我深表同情(加大力度)!面试

我差点笑出了声

好了,不开玩笑了,在和小强的面试题中,我选取了几个比较典型的线程和线程池的问题。数组

Java中的线程和操做系统的线程有什么关系?多线程

调用start方法是如何执行run方法的?并发

线程池提交任务有哪几种方式?分别有什么区别?操作系统

谈谈你对阻塞队列的理解。线程

常见的线程池有哪些?为何阿里不容许使用 Executors 去建立线程池?3d

线程池任务调度的流程大体讲一下。code

线程池里面的线程执行异常了会怎么样?

核心线程和非核心线程是如何区分的?

想要答对这些问题,并非很难,可是想要答好,我以为是很是考验我的功底的。

为了弄清这些问题,我连夜加急,采访了“线程”,下面是线程的自述。

我是谁

我是一个线程,一个底层的打工人。

打工人

总有人把我和进程搞混,但其实我和进程的区别很大。

进程是程序的一次执行,CPU的资源都是分发给进程而不是分发给咱们线程,进程是资源分配的最小单位,一个进程能够包含不少向我这样的线程。

咱们线程是CPU调度执行的最小单位,真正的打工人。

Java中的线程

在Java里面,个人名字叫作java.lang.Thread。

须要注意的是,调用run方法和执行一个普通方法没有区别。想要真正的建立一个线程并启动,须要调用个人start方法。

f0b45f3125b59f52e34f562240e9a1ff

有一点我必须告诉你,就是我也是有小弟的。

在JVM里面,我有一个JavaThread的小弟,他帮我联系操做系统的osthread线程。

调用个人start方法以后,具体的执行流程是这样的:

image-20210130184446156

固然了,这个过程省略了不少细节,不过很明确的是,我和内核线程是一一对应的。

调度我就至关于调度内核线程,而调度内核线程须要在用户态和内核态之间切换,这个过程开销是很是大的。

因此,建立我成本是很高的,必定要慎重。

Untitled Diagram

线程池

和大家人类同样,我也有着精彩的一辈子,也会经历出生(建立)、奋斗(Running)、死亡(销毁)等过程,今天我主要和你讲述的是我打工奋斗的生活。

原来我是打零工的,有人须要个人时候就建立一个我,等我完成工做就把我销毁。

image-20210130172133624

上面也提到过,我和内核线程是一对一的,建立和销毁的过程是很是消耗资源的,因此这样的成本很是高。

因而,有人就想了一个办法,开了一个公司,也就是大家说的线程池。

线程池公司统一管理调度咱们线程。咱们在线程池里面重复着等待工做——完成工做的步骤。

image-20210130175632614

这样我就能够日复一日年复一年的重复打工了,这种提供了减小对象数量从而改善应用所需的对象结构的方式的模式,被大家人类叫作“享元模式”。

线程池公司有不少种,但都离不开这几个主要指标:

image-20210130175721394
  • corePoolSize:公司正式员工人数。
  • maximumPoolSize:正式工+临时工最大数量。
  • keepAliveTime:临时工多久没作事情会被开除。
  • unit:临时工没作事情会被开除的时间单位。
  • workQueue:公司业务接收部门。
  • threadFactory:行政部,负责招聘培训员工的。
  • handler:业务部接收业务到达上限了的处理方式。

阻塞队列

线程池中的workQueue是一个阻塞队列,用于存放线程池未能及时处理执行的任务。

它的存在既解耦了任务的提交与执行,又能起到一个缓冲的做用。

阻塞队列有不少,下面我带你了解一下常见的阻塞队列。

ArrayBlockingQueue

基于数组实现的有界阻塞队列,建立的时候须要指定容量。此类型的队列按照FIFO(先进先出)的规则对元素进行排序。

ArrayBlockingQueue

LinkedBlockingQueue

基于链表实现阻塞队列,默认大小为Integer.MAX_VALUE。按照FIFO(先进先出)的规则对元素进行排序

LinkedBlockingQueue

SynchronousQueue

一个不存储元素的阻塞队列。每个put操做必须阻塞等待其余线程的take操做,take操做也必须等待其余线程的put操做。

SynchronousQueue_2

PriorityBlockingQueue

一个基于数组利用堆结构实现优先级效果的无界队列,默认天然序排序,也能够本身实现compareTo方法自定义排序规则。

PriorityBlockingQueue

DelayedWorkQueue

一个实现了优先级队列功能且实现了延迟获取的无界队列,在建立元素时,能够指定多久多久才能在队列中获取当前元素。只有延时期满了后才能从队列中获取元素。

DelayedWorkQueue

拒绝策略

当任务队列满了以后,若是还有任务提交过来,会触发拒绝策略,常见的拒绝策略有:

  • AbortPolicy:丢弃任务并抛出异常,默认该方式。

  • CallerRunsPolicy:由调用线程本身处理该任务。谁调用,谁处理。

image-20210131164350267
  • DiscardPolicy:丢弃任务,可是不抛出异常。

  • DiscardOldestPolicy:抛弃任务队列中最旧的任务也就是最早加入队列的,再把这个新任务添加进去。先从任务队列中弹出最早加入的任务,空出一个位置,而后再次执行execute方法把任务加入队列。

image-20210131164701335

固然,除了以上这几种拒绝策略,你也能够根据实际的业务场景和业务需求去自定义拒绝策略,只须要实现RejectedExecutionHander接口,自定义里面的rejectedExecution方法。

运行流程

咱们每一个线程会被包装成Worker,线程池里面有一个HashSet存放Worker。

当有任务提交过来以后:

  1. 首先检测线程池运行状态,若是不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 若是线程池中Worker的数量小于核心线程数,就会去建立一个新的线程,也就是招聘一个正式工让他执行任务。
  3. 若是Worker的数量大于或者等于核心线程数,就会把任务放到阻塞任务队列里面。
  4. 若是任务队列满了还有任务过来,若是临时工名额没有满(workerCount < maximumPoolSize),就去招聘临时工让临时工执行任务。若是临时工名额都满了,触发任务拒绝策略。
image-20210131184333322

总结而言,就是核心线程能干的事情尽可能不去建立非核心线程,这是线程池很关键的一点。

new ThreadPoolExecutor(4,  8, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(4));

以这个线程池为例,下面是他的任务提交和执行流程:

恢复_线程池_3

有哪些线程池

我有过四段工做经历,每段经历都有着精彩的故事。

SingleThreadExecutor

SingleThreadExecutor是我加入的第一家线程池,这是一家创业公司,整个线程池就只有我一个线程。

image-20210131022002342
SingleThreadExecutor_2

全部的任务都由我干,并且任务队列是一个无界队列。就是说,打工的线程只有我一个,可是需求任务能够是无限多。

在需求任务不少的时候,常常出现任务处理不过来的状况,致使任务堆积,出现OOM。

但由于全部的活都是我干,没有繁琐的沟通成本,不须要处理线程同步的问题,这算是这种线程池的一个优势吧。

这种线程池适用于并发量不大且须要任务顺序执行的场景。

FixedThreadPool

后来公司倒闭了,我又加入了一个叫FixedThreadPool的线程池。

image-20210131021901279
FixedThreadPool

FixedThreadPool和SingleThreadExecutor惟一不一样的地方就是核心线程的数量,FixedThreadPool能够招收不少的打工线程。

在这里,我再也不是孤军奋斗了,我有了一群共同打拼的小伙伴,你们一块儿完成任务,一块儿承担压力。

可这种线程池仍是存在一个问题——任务队列是无界的,需求任务过多的话,仍是会形成OOM。

这种线程池线程数固定,且不被回收,线程与线程池的生命周期同步的线程池,适用于任务量比较固定但耗时长的任务。

CachedThreadPool

后来,为了离家更近,我离职了。加入了一家叫CachedThreadPool的线程池,进去以后,却发现这是一家外包公司。

image-20210131015429257
CachedThreadPool

这种线程池里面没有一个核心线程(正式工),一有需求就去招聘一个非核心线程(临时工)。

若是一个线程任务干完了以后,60秒以后没有新的任务就会被辞退。

这种线程池的任务队列采用的是SynchronousQueue,这个队列是没法插入任务的,一有任务就建立一个线程执行,若是并发高且任务耗时长,建立太多线程也是可能致使OOM的。因此CachedThreadPool比较适合任务量大但耗时少的任务。

ScheduleThreadPool

经历了外面的风风雨雨,我以为仍是找份固定的工做比较可靠,因而我加入了一家叫作ScheduleThreadPool的国企。

image-20210131022031076
ScheduleThreadPool

在这里,工做比较的轻松,多数状况下,我只须要在固定的时间干固定的活。

任务忙不过来的时候,公司也会招聘一些临时工帮忙处理,临时工干完活就会被辞退。

综合来讲,这类线程池适用于执行定时任务和具体固定周期的重复任务。因为采用的任务队列是DelayedWorkQueue无界队列,因此也是有OOM的风险的。

总结

好了,关于线程的故事就告一段落了。关于线程池的应用实践,咱们下次再聊。

文章开头的面试题在大部分在文中都能找到答案,对于没有提到的,这里作一个补充:

1. 线程池提交任务有哪几种方式?分别有什么区别?

有execute和submit两种方式

  • execute只能提交Runnable类型的任务,无返回值。submit既能够提交Runnable类型的任务,也能够提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。

  • execute在执行任务时,若是遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。

2. 线程池里面的线程执行异常了会怎么样?

若是一个线程执行任务的过程当中出现异常,那么这个线程对应的Worker会被移出线程池,该线程也会被销毁回收。

同时会经过指定的线程工厂建立一个线程,并封装成Worker放入线程池代替移除的Worker。

image-20210201045911590

3. 核心线程能被回收吗?

核心线程默认不会被回收。可是能够调用allowCoreThreadTimeOut让核心线程能够被回收。

image-20210131190101003

须要注意的是,调用这个方法的线程池必须将keepAliveTime设置为大于0,不然会抛出异常。

image-20210131190226646

4. 核心线程和非核心线程是如何区分的?

核心线程和非核心线程是一个抽象概念,只是用于更好的表述线程池的运行逻辑,实际上都对应操做系统的osThread,都是重量级线程。

在新增Worker的时候,经过一个boolean表达是核心线程仍是非核心线程,本质上二者没有什么不一样。

image-20210131205854199

5. 为何阿里不容许使用 Executors 去建立线程池?

FixedThreadPool 和 SingleThreadPool:容许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而致使 OOM。

CachedThreadPool:容许的建立线程数量为 Integer.MAX_VALUE,可能会建立大量的线程,从而致使 OOM。

总结来讲就是,使用Executors建立线程池会容易忽视线程池的一些属性,使用不当容易引发资源耗尽。

写在最后

这个世界上或许没有线程,又或许人人都是线程。

无畏年少青春,迎风潇洒前行,作一个努力奋斗的线程,但愿他日回首望去,不是一片黑暗,而是漫天星光。

好了,今天的文章就到这里了。

最后,感谢你的阅读!

我是CoderW,一个普通的程序员。

点个关注,咱们下期再见!

image-20210131205854199