几种线程池的实现算法分析【转载】

 

原文地址html

本文内容

  • 前言
  • 线程池意义
  • 线程池技术要点
  • 小节
  • 参考源码

但凡是一个框架(“服务”框架),基本都会涉及线程池问题。虽然你可能没有直接使用它,但这是由于框架帮你完成了这部分工做。java

说,为何须要线程池呢?试想,如今但凡是写一个服务程序,若是不采用并发或并行的方式,都有点对不起4核、8核,甚至更多的CPU内核(物理内核,逻辑内核)。若是每次须要线程,都建立一次,使用完后,销毁,对系统性能的消耗比较大,更加合适的作法是,在程序初始化时,一次性把全部线程都建立好,这样,当须要的时候,直接用就行~python

虽然框架提供线程池,不须要本身写,但了解线程池,至少能大幅度提升你的编程能力。可能,不是每一个项目都要搞一套框架,若是项目没那么复杂,本身写个简单的线程池仍是须要的。算法

迁移到:http://www.bdata-cap.com/newsinfo/1828966.htmlapache

前言


在阅读研究线程池的源码以前,一直感受线程池是一个框架中最高深的技术。研究后才发现,线程池的实现是如此精巧。本文从技术角度分析了线程池的本质原理和组成,同时分析了JDK、Jetty六、Jetty八、Tomcat的源码实现,对于想了解线程池本质、更好的使用线程池或者定制实现本身的线程池的业务场景具备必定指导意义。编程

线程池意义


  • 复用:相似Web服务器等系统,内部须要使用大量的线程处理客户端请求,而单次请求响应时间一般比较短,此时Java基于操做系统的本地调用方式大量的建立和销毁线程自己会成为系统的一个瓶颈和资源浪费。若使用线程池技术能够实现工做线程的复用,即一个工做线程建立和销毁的生命周期期间内能够执行处理多个任务,从整体上下降线程建立和销毁的频率和时间,提高了系统性能。
  • 流控:服务器资源有限,超过服务器性能的太高并发设置反而成为系统的负担,形成CPU大量耗费于上下文切换、内存溢出等后果。经过线程池技术能够控制系统最大并发数和最大处理任务量,从而很好的实现流控,保证系统不至于崩溃。
  • 功能:JDK的线程池实现的很是灵活,并提供了不少功能,一些场景基于功能的角度会选择使用线程池。

线程池技术要点


从内部实现上看,线程池技术可主要划分为以下6个要点实现:数组

0723010

图1 线程池技术要点tomcat

  • 工做者线程worker

线程池中的线程能够重复利用执行任务,一个worker在生命周期内会不停的处理多个任务job,本质上,就是复用一个worker去处理多个job服务器

“流控“本质是经过对worker数量的控制实现并发数控制。经过设置参数控制worker的数量,实现线程池的容量伸缩。并发

  • 待处理job的存储队列

工做者线程workers的数量是有限的,同一时间最多只能处理workers数量个job。对于来不及处理的job须要保存到等待队列,空闲的工做者worker会不停的读取队列里的job进行处理。

基于不一样的队列,能够实现多种线程池,如定制队列出队顺序实现带处理优先级的线程池、定制队列为阻塞有界队列实现可阻塞能力的线程池等。

流控一方面经过控制worker数控制并发数和处理能力,一方面可基于队列控制线程池处理能力的上限。

  • 线程池初始化

线程池参数的设定和多个工做者workers的初始化。一般有一开始就初始化指定数量的workers或者有请求时逐步初始化工做者两种方式。前者线程池启动初期响应会比较快但形成了空载时的少许性能浪费,后者是基于请求量灵活扩容但牺牲了线程池启动初期性能达不到最优。

  • 处理业务job算法

业务给线程池添加任务job时线程池的处理算法。有的线程池基于算法识别直接处理job,仍是增长工做者数量来处理job,或者放入待处理队列,也有的线程池会直接将job放入待处理队列,等待工做者worker去取出执行。

  • workers增减算法

业务线程数不是持久不变的,有高低峰期。线程池要有本身的算法根据业务请求频率高低调节自身工做者workers的数量,从而调节线程池大小,实现业务高峰期增长工做者数量提升响应速度,而业务低峰期减小工做者数来节省服务器资源。增长算法一般基于几个维度进行:待处理工做job数、线程池定义的最大最小工做者数、工做者闲置时间。

  • 线程池终止逻辑

中止时线程池要有自身的中止逻辑,保证全部job都获得执行或者抛弃。

几种线程池的实现


结合上面的技术点,列举几种线程池实现方式。

工做者workers与待处理工做队列实现方式举例

表 1

实现

工做者workers结构与并发保护

待处理工做队列结构

JDK

使用了HashSet来存储工做者workers,经过可重入锁ReentrantLock对其进行并发保护。每一个worker都是一个Runnable接口。

使用了实现接口BlockingQueue的阻塞队列来存储待处理工做job,并把队列做为构造函数参数,从而实现业务能够灵活的扩展定制线程池的队列。

业务也可以使用JDK自身的同步阻塞队列SynchronousQueue、有界队列ArrayBlockingQueue、无界队列LinkedBlockingQueue、优先级队列PriorityBlockingQueue

Jetty6

一样使用了HashSet存储工做者workers,经过synchronized一个对象进行HashSet的并发保护。每一个工做者其实是一个Thread的扩展。

使用了数组存储待处理的job对象Runnable。数组初始化容量为_maxThreads个,使用变量_queued计算保存当前内部待处理job的个数即数组length。超过数组最大值时,扩大_maxThreads个容量,所以数组永远够用够大,容量无界。一样是用synchronized一个对象的方式实现同步。

Jetty8

使用了ConcurrentLinkedQueue存储工做者workers,利用JDK基于CAS算法的实现提升了并发效率,同时也下降了线程池并发保护的复杂程度。

针对队列ConcurrentLinkedQueue没法保证size()实时性问题引入原子变量AtomicInteger统计工做者数量。

与JDK相同实现,使用了基于接口BlockingQueue的阻塞队列来存储待处理工做job,也支持在线程池构造函数的参数中传入队列类型。同时,Jetty8内部默认未设置队列类型场景可自动设置使用2种队列:有界没法扩容的ArrayBlockingQueue,以及Jetty自身定制扩展实现的可扩容队列BlockingArrayQueue

Tomcat

基于JDK的ThreadPoolExecutors实现,复用JDK业务

复用JDK业务

线程池初始化与处理业务job算法举例

表 2

实现

线程池构造与工做者初始化

处理业务job的算法

JDK

1. 基于多个构造参数实现灵活初始化,几个核心参数以下:

corePoolSize:核心工做者数

maximumPoolSize:最大工做者数

keepAliveTime:超过核心工做者数时闲置工做者的存活时间。

workQueue:待处理job队列,即前面提到的BlockingQueue接口。

2. 默认初始化后,不启动工做者,等待有请求时才启动。能够经过调用线程池接口提早启动核心工做数指定的工做者线程,也能够启动业务指望的多个工做者线程。

1. 工做者workers数量低于核心工做者数corePoolSize时,会优先建立一个工做者worker处理job,处理成功则返回。

2. 工做者workers数量高于核心工做者数时,会优先把job放入到待处理队列,放入队列成功时处理结束。

3. 步骤2中入队失败会识别工做者数是否还小于最大工做者数maximumPoolsize,小于的话也会新建立一个工做者worker处理job。

4. 拒绝处理

Jetty6

1. 支持设置多个参数:

_spawnOrShrinkAt:扩容/缩容阀值

_minThreads:最小工做者数

_maxThreads:最大工做者数

_maxIdleTimeMs:闲置工做者最大闲置超时时间

2. 初始化后直接启动_minThreads个工做者线程

1. 查找闲置的工做者worker,找到则分配job。

2. 没有闲置的工做者,将job存入待处理数组。

3. 当识别到数组中待处理job超过扩容阀值参数时,扩容增长工做者处理job

4. 不然不处理

Jetty8

1. 配置参数相似Jetty6,但去除了_spawnOrShrinkAt阀值参数。

2. 初始化后直接启动_minThreads个工做者线程

直接将待处理job入队。

Tomcat

1. 基于JDK线程池的构造方法

2. 来请求时启动工做者

处理方法复用JDK的,可是在开始提交前扩展了JDK的功能,实现了能够统计提交数submittedCount的能力

线程池工做者worker的增减机制举例

表 3

实现

工做者增长算法

工做者减小算法

JDK

1. 待处理job来时,工做者workers数量低于核心工做者数corePoolSize时。

2. 待处理job来时,workers数超过核心数小于最大工做者数且入待处理队列失败场景。

3. 业务调用线程池的更新核心工做者数接口时,若发现扩容,会增长工做者数。

1. 待处理任务队列里没有job而且工做者workers数量超过了核心工做者数corePoolSize。

2. 待处理任务队列里没有job而且容许工做者数量小于核心工做者参数为true,此场景会至少保留一个工做者线程。

Jetty6

1. 启动线程池时会启动_minThreads个工做者线程

2. 待处理的job数量高于了阀值参数且工做者数没有达到最大值时会增长工做者。

3. 调用线程池接口setMinThreads更新最小工做者数时会根据须要增长工做者。

以下三个条件同时知足时会减小工做者:

1. 待处理任务数组中没有待处理job。

2. 工做者workers数量超过了最小工做者数_minThreads。

3. 闲置工做者线程数高于了阀值参数。

Jetty8

1. 启动线程池时启动最小工做者参数个工做者线程。

2. 已经没有闲置工做者或者闲置工做者的数量已经小于待处理的job的总数。

3. 调用线程池接口setMinThreads更新最小工做者数时。

以下三个条件同时知足时会减小工做者:

1. 待处理任务队列里没有待处理的job。

2. 工做者workers总数超过了最小工做者参数配置_minThreads。

3. 工做者线程的闲置时间超时。

Tomcat

同JDK增长工做者算法

复用JDK减小算法,同时定制扩展延迟参数,超过参数时,直接抛出异常到外面来终止线程池工做者。

小结


对比几种线程池实现,JDK的实现是最为灵活、功能最强且扩展性最好的。Tomcat即基于JDK线程池功能扩展实现,复用原有业务的同时扩充了本身的业务。Jetty6是彻底本身定制的线程池业务,耦合线程池众多复杂的业务逻辑到线程池类里面,逻辑相对最为复杂,扩展性也很是差。Jetty8相对Jetty6的实现简化了不少,其中利用了JDK中的同步容器和原子变量,同时实现方式也愈来愈接近JDK。

参考源码


  • JDK源码类:java.util.concurrent.ThreadPoolExecutor
  • Jetty6源码类:org.mortbay.thread.QueuedThreadPool
  • Jetty8源码类:org.eclipse.jetty.util.thread.QueuedThreadPool
  • Tomcat源码类:org.apache.tomcat.util.threads.ThreadPoolExecutor
  • python 线程池
  • Quartz.Net SimpleThreadPool 和 ZeroSizeThreadPool
相关文章
相关标签/搜索