dcddc

西米大人的博客

0%

系统学习线程池

线程池概述

由来
为减少频繁创建和销毁线程的开销,引入的概念:提前定制一定量的资源来进行资源复用

作用
1、控制线程数量。2、工作线程可被复用。3、线程池参数调优,高效利用资源。

核心类介绍

ExecutorService

线程池上层接口

ThreadPoolExecutor

ExecutorService的默认实现,供Executors工厂类创建线程池的底层使用

构造参数含义:

  • corePoolSize:池中所保存的线程数,包括空闲线程。
  • maximumPoolSize:池中允许的最大线程数。
  • keepAliveTime:当线程数大于核心时,此为终止多余的空闲线程前允许其等待新任务的最长时间。
  • unit:keepAliveTime参数的时间单位。
  • blockingQueue:执行前用于保存任务的队列。此队列仅保持由execute方法提交的Runnable任务。
  • threadFactory:创建新线程时使用的工厂。
  • rejectedExecutionHandler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

blockingQueue

工作原理

如果运行的线程少于corePoolSize,则Executor始终首选添加新的线程,而不进行排队。
如果运行的线程等于或多于corePoolSize,则Executor始终首选将请求加入队列,而不添加新的线程。
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

任务的三种排队策略

1、直接提交。
工作队列的默认选项是SynchronousQueue,在添加任务后必须等待其他线程取走后才能继续添加。如果不存在可用于立即运行任务的线程,且线程数小于池子最大数,会构造一个新的线程。这一点很重要,核心线程打满时,在同步队列里的任务会立即触发额外线程来处理任务,由此保证提交到线程池的任务立即顺序执行,避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSizes以避免拒绝新提交的任务。

2、无界队列。
使用无界队列(例如,不具有预定义容量的LinkedBlockingQueue)将导致在所有corePoolSize线程都忙时新任务在队列中等待。这样,创建的线程就不会超过corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在Web服务器中。这种排队可用于处理瞬态突发请求。

3、有界队列。
当使用有限的maximumPoolSizes时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

4、延时队列。
定时线程池使用延时队列DelayedWorkQueue实现定时执行任务。延时队列基于最小堆构造,最近要执行的延时任务放在堆顶,内部有一个leader线程指向等待最近一个要执行任务的线程

offer方法提交延时任务:

  • 任务入最小堆,如果任务在堆顶,说明是最近一个要执行的任务,则唤醒一个等待在延时队列的线程,让它作为leader线程,再执行awaitNanos方法等待最近的延时任务

take方法获取延时任务:

  • 堆顶无任务,则线程await,等待在延时队列
  • leader线程不为空说明有任务在等待最近一次延时任务,则线程await,等待在延时队列
  • 否则,当前线程作为leader线程,等待堆顶的最近一次延时任务,即执行awaitNanos方法延时等待
    • 期间如果有更紧急的延时任务提交,则leader线程会置空,并唤醒一个等待在延时队列的线程,作为新的leader线程等待在最近的延时任务上
  • 获取到延时任务后,leader线程置空,并唤醒一个等待在延时队列的线程,作为leader线程继续等待下一个延时任务

RejectedExecutionHandler

jdk提供了四种任务拒绝策略RejectedExecutionHandler的实现

CallerRunsPolicy

线程池未关闭时,在线程池运行的主线程中执行被拒绝任务。
这种策略提供了一个简单的反馈控制机制,缺点就是可能会阻塞主线程

AbortPolicy

拒绝任务时会抛出运行时RejectedExecutionException到线程池运行的主线程中
它是线程池默认的拒绝策略

DiscardPolicy

直接丢弃任务,和AbortPolicy的唯一区别就是AbortPolicy抛出异常,DiscardPolicy不做任何处理

DiscardOldestPolicy

线程池未关闭时,丢弃阻塞队列中最老的(头部)任务,然后再将该任务提交到线程池中执行

Executors

创建线程池的一个工厂类

1.newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

core=1,max=1,keepAliveTime=0,LinkedBlockingQueue

2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到达到线程池的最大线程数。一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

core=n,max=n,keepAliveTime=0,LinkedBlockingQueue

3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

core=0,max=MAX_VALUE,keepAliveTime=60s,SynchronousQueue

4.newScheduledThreadPool
创建一个定时线程池。此线程池支持定时以及周期性执行任务的需求

core=n,max=MAX_VALUE,keepAliveTime=10ms,DelayedWorkQueue

线程池注意事项

1、并发可能产生的死锁问题
2、线程泄漏:使用局部线程池造成线程泄漏

  • 简单理解,线程池里的线程执行完任务后并不会stop,而是在while循环里不断轮询阻塞队列(请求队列)拿任务
  • ThreadPoolExecutor -> Worker -> Thread,由于存在这样的引用关系,并且Thread作为GC Root,所以无法被回收

线程池工作模型

1、线程池管理器

  • 核心是管理工作线程
    • 构造线程池时,初始化一些工作线程,添加到工作线程队列并启动
    • 接收外部任务,提交到请求队列。请求队列拒绝时,比较工作线程队列的线程数和核心线程数、最大线程数的关系,决定是创建新的工作线程还是拒绝任务
    • 关闭线程池时,停止所有工作线程

2、请求队列

  • 核心是服务于工作线程取任务
    • 提交到线程池的任务,同步添加到请求队列
      • 添加后notify所有等待在请求队列的工作线程取任务
    • 提供接口给工作线程,同步从请求队列取任务
      • 请求队列为空时,wait在请求队列
    • 请求队列满时,调用拒绝策略

3、工作线程

  • 核心是从请求队列里取任务执行
    • 轮询请求队列,获取并执行任务

4、工作线程队列

  • 核心是维护创建的工作线程
    • 负责工作线程的入队和出队