线程池的基本工作流程和核心参数
线程池构造函数中的核心参数十分重要,理解好了这几个核心参数,是使用线程池和了解其原理的必要条件,因为不同的参数,会导致线程池的工作状态不同。
从ThreadPoolExecutor的构造函数可以看到,里面有7个核心参数,下面针对每个参数进行说明一下。
- corePoolSize: 核心池大小(运行线程)
- maximumPoolSize:池中允许的最大线程,这个参数表示了线程池中最多的线程数量,当队列满了之后,会判根据maximumPoolSize判断是否需要创建新的线程执行任务
- keepAliveTime:当线程大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间
- unit:上一个参数的时间单位
- workQueue:存储还没来的及执行的任务,这个参数十分重要,因为使用不通的队列,会导致线程池不一样,例如使用了无界队列,那么永远不会进行扩容,所以需要知道java中队列的类型,并且根据实际情况,使用不同的队列
- threadFactory:执行程序创建新线程时使用的工厂
- handler:由于超出线程范围和队列容量而使执行被阻塞时使用的处理程序
对于corePoolSize、maximumPoolSize、workQueue三个参数,需要着重的进行理解,那么下面运行一个例子,不通情况下,线程池运行的情况,废话不多,撸代码。
首先定义一个工作线程
定义线程池,核心线程数1,最大线程数为2,队列为ArrayBlockingQueue有节队列大小为3。
运行结果如下:
可以看到,四个任务都是由一个先成为完成的,因为分发任务数=核心线程数+队列SIZE,所以不会创建线程进行处理任务。那么我们在添加一个任务会出现什么效果呢?
这时候会发现,有两个线程在工作,因为当向线程池中放入“任务5”的时候,线程池的队列已满,所以就会根据最大线程数,创建一个新的线程,进行工作。所以会存在2个线程工作的场景,同时如果线程2,在60秒钟没有工作的话,那么就会被销毁。
那么在放入“任务6”会出现什么效果呢?
这时候会抛出异常,因为我们的拒绝策略是AbortPolicy。
从上面的分析可以得出结论,线程池工作原理如下,首先查看核心线程是否还有空余,有就直接执行任务,当没有空余的时候,就放入队列。当队列满了之后,新来的任务就会根据最大线程数,进行线程的创建去执行任务(这里创建的线程都是根据参数设置时间进行回收的)。当超过最大线程数的时候,就会根据拒绝策略进行相关的操作。流程图如下:
核心参数中的核心线程数(corePoolSize),最大线程数(maximumPoolSize),从上面的描述已经很清楚,还有另外两个参数分别是阻塞队列、拒绝策略,这两个参数的不通会导致线程池操作的不通,例如队列使用了无界队列,那么整个线程池永远都不会进行扩容。
1、队列
java中提供了很多种队列,线程池中使用的时候阻塞队列(因为队列里面的任务是需要等待其他任务执行完毕才能执行,如果使用非阻塞队列,那么队列里面的任务永远都不会执行成功了),借着这个机会,同时介绍一个阻塞队列、非阻塞队列、双向队列。在介绍各种队列之前,先介绍几个名词。
有界、无界:就是队列是否存在固定大小,如果一个队列是有界的,那么在放入指定个数的元素后就无法再次放入了。如果一个队列是无界的,那么可以持续一直放入元素(底层也是有界队列,只不过当达到上限后,会扩容)。
阻塞、非阻塞:如果一个队列是阻塞的,在读取的时候当队列为空,会按照设置时间进行阻塞,添加的时候也会按时间进行阻塞。非阻塞则在添加、读取的时候不进行阻塞。
java中分为 Queue、Dueqe两种类型的队列,下面分别介绍一下它们的不同:
Queue :只能从头部取元素、插入元素到尾部(单向)
Deque:可以同时在头部、尾部插入和取出元素(双向)
Queue
非阻塞:
① PriorityQueue:无界优先级队列,放入元素必须能比较大小,否则抛出异常,按照自然大小排序。
② ConcurrentLinkedQueue :无界线程安全的队列,基于链表实现,可在多线线程访问中使用,(简单看了一下源码,采用CAS底层实现)
阻塞(BlockingQueue ):
① DelayQueue :无界阻塞延时队列,这里需要解释一下延时的作用,就是入列元素需要经过一段时间才能被访问。
② SynchronousQueue :同步队列,其实就是只有一个一个元素的阻塞队列。
③ LinkedBlockingQueue :基于链表的有界阻塞队列
④ ArrayBlockingQueue :基于数组的有界阻塞队列
⑤ PriorityBlockingQueue :无界优先级阻塞队列
Deque
非阻塞:
① LinkedList :我们常用的链表
② ArrayDeque:线程不安全的,基于数组的双向队列,大小可变
③ ConcurrentLinkedDeque:线程安全的无界双向队列
阻塞:
① LinkedBlockingDeque:基于链表的有界阻塞双向队列
上面简单的介绍了java中相关队列的实现,以及每个队列的特点,并没有进行源码分析和撸代码,毕竟是讲线程池,同时相信大家根据上面队列特性的不通,对线程池中 BlockingQueue 传递也有一定了解了。
2、拒绝策略
线程池拒绝策略一共有四种(他们的源码很简单,一目了然,是ThreadPoolExecutor的内部类),比较简单,它们分别是:
① AbortPolicy:线程池的默认策略,丢弃并抛出异常 RejectedExecutionException
② DiscardPolicy:直接丢弃任务
③ DiscardOldestPolicy:丢弃线程池中最老的任务,让最新的任务入列
④ CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
开发人员也可以根据自己的需要,自定义拒绝策略,只需要实现 RejectedExecutionHandler接口即可。
Executors工具类
为了更好的了解他们的组合使用,下面简单说明一下 Executors 这个类。这个类就是对ThreadPoolExecutor的构造函数进行简单封装,使用不同的参数实现不同的线程池功能,同时在生产中不建议使用该类,还是要根据自己实际的业务情况对参数进行设置。
1、newFixedThreadPool :固定大小的线程池,队列可以理解为无界的,因为默认值太大了,所以基本不会涉及到扩容,同时因为corePoolSize=maxPoolSize,也不会扩容。
2、newSingleThreadExecutor:整个线程池只有一个线程,因此队列中的任务可以按照顺序执行
3、newCachedThreadPool:没有核心线程,基本算是无限制的创建线程进行任务处理,当60秒钟没有执行任务的时候,就会回收线程。
4、ScheduledThreadPoolExecutor:按照执行的策略进行执行,可以看到最大线程接近无限大,队列采用延迟队列。
5、newWorkStealingPool jdk1.8之后新增的线程池,底层用的ForkJoinPool 来实现的。 ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。
工作窃取:就是闲置的CPU查看是否能帮忙其他的CPU执行任务。
源码分析,以看看这篇文章,非常棒写的很详细:传送门