自己动手实现线程池 jdk线程池ThreadPoolExecutor工作原理解析(一)

jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一)线程池介绍在日常开发中经常会遇到需要使用其它线程将大量任务异步处理的场景(异步化以及提升系统的吞吐量),而在使用线程的过程中却存在着两个痛点 。

  1. 在java等很多主流语言中每个逻辑上的线程底层都对应着一个系统线程(不考虑虚拟线程的情况) 。操作系统创建一个新线程是存在一定开销的,在需要执行大量的异步任务时 , 如果处理每个任务时都直接向系统申请创建一个线程来执行,并在任务执行完毕后再回收线程,则创建/销毁大量线程的开销将无法忍受 。
  2. 每个系统线程都会占用一定的内存空间,且系统在调度不同线程上下文切换时存在一定的cpu开销 。因此在一定的硬件条件下 , 操作系统能同时维护的系统线程个数相对而言是比较有限的 。在使用线程的过程中如果没有控制好流量,会很容易创建过多的线程而耗尽系统资源 , 令系统变得不可用 。
而线程池正是为解决上述痛点而生的,其通过两个手段来解决上述痛点 。
池化线程资源池化线程资源,顾名思义就是维护一个存活线程的集合(池子) 。提交任务的用户程序不直接控制线程的创建和销毁,不用每次执行任务时都申请创建一个新线程,而是通过线程池间接的获得线程去处理异步任务 。线程池中的线程在执行完任务后通常也不会被系统回收掉 , 而是继续待在池子中用于执行其它的任务(执行堆积的待执行任务或是等待新任务) 。线程池通过池化线程资源,避免了系统反复创建/销毁线程的开销,大幅提高了处理大规模异步任务时的性能 。
对线程资源的申请进行收口,限制系统资源的使用如果程序都统一使用线程池来处理异步任务,则线程池内部便可以对系统资源的使用施加一定限制 。例如用户可以指定一个线程池最大可维护的线程数量,以避免耗尽系统资源 。当用户提交任务的速率过大 , 导致线程池中的线程数到达指定的最大值时依然无法满足需求时,线程池可以通过丢弃部分任务或限制提交任务的流量的方式来处理这一问题 。线程池通过对线程资源的使用进行统一收口,用户可以通过设置线程池的参数来控制系统资源的使用 , 从而避免系统资源耗尽 。
jdk线程池ThreadPoolExecutor简单介绍前面介绍了线程池的概念,而要深入理解线程池的工作原理最好的办法便是找到一个优秀的线程池实现来加以研究 。而自jdk1.5中引入的通用线程池框架ThreadPoolExecutor便是一个很好的学习对象 。其内部实现不算复杂,却在高效实现核心功能的同时还提供了较丰富的拓展能力 。
下面从整体上介绍一下jdk通用线程池ThreadPoolExecutor的工作原理(基于jdk8) 。
ThreadPoolExecutor运行时工作流程首先ThreadPoolExecutor允许用户从两个不同维度来控制线程资源的使用,即最大核心线程数(corePoolSize)和最大线程数(maximumPoolSize) 。最大核心线程数:核心线程指的是通常常驻线程池的线程 。常驻线程在线程池没有任务空闲时也不会被销毁,而是处于idle状态,这样在新任务到来时就能很快的进行响应 。最大线程数:和第一节中提到的一样,即线程池中所能允许的活跃线程的最大数量 。
在向ThreadPoolExecutor提交任务时(execute方法),会执行一系列的判断来决定任务应该如何被执行(源码在下一节中具体分析) 。
  1. 首先判断当前活跃的线程数是否小于指定的最大核心线程数corePoolSize 。如果为真,则说明当前线程池还未完成预热,核心线程数不饱和,创建一个新线程来执行该任务 。如果为假,则说明当前线程池已完成预热 , 进行下一步判断 。
  2. 尝试将当前任务放入工作队列workQueue(阻塞队列BlockingQueue),工作队列中的任务会被线程池中的活跃线程按入队顺序逐个消费 。如果入队成功,则说明当前工作队列未满,入队的任务将会被线程池中的某个活跃线程所消费并执行 。如果入队失败,则说明当前工作队列已饱和,线程池消费任务的速度可能太慢了 , 可能需要创建更多新线程来加速消费,进行下一步判断 。
  3. 判断当前活跃的线程数是否小于指定的最大线程数maximumPoolSize 。如果为真,则说明当前线程池所承载的线程数还未达到参数指定的上限,还有余量来创建新的线程加速消费,创建一个新线程来执行该任务 。如果为假,则说明当前线程池所承载的线程数达到了上限,但处理任务的速度依然不够快,需要触发拒绝策略 。

    推荐阅读