线程池ThreadPoolExecutor源码学习

123 阅读7分钟

线程池ThreadPoolExecutor源码解析

一.池化技术

  • 随着计算机的发展,摩尔定律逐渐失效,多核CPU成为主流,使用多线程并行计算充分利用多核特点,最大化利用系统资源,而线程池就是基于池化思想管理线程和任务的工具

  • 池化技术的思想主要是为了减少每一次获取资源的损耗,提高资源的利用,主要应用有,连接池,线程池,内存池,实例池

  • 使用线程池的好处:

    • 降低资源消耗:通过重复利用已创建的线程来降低线程创建与销毁的资源消耗
    • 提高响应速度: 当任务到达是,可以不需要等待线程创建就能立即执行
    • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一的分配,监控和调优
    • 提供更多更强大的功能:线程池具备可扩展性,允许开发人员增加更多的功能
  • 解决什么问题? 线程池解决的核心问题就是资源管控问题 1.频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大 2.对资源无限申请缺少抑制手段,容易引起系统资源耗尽的风险 3.系统无法管控内部资源分布,会减低系统的稳定性

二.创建线程池的方式

  • 使用Executors框架创建

    • ExecutorService executorService = Executors.newFixedThreadPool(10);

    • newFixedThreadPool

      • 固定大小线程池:创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程,底层调用new ThreadPoolExecutor()进行线程池的创建

      • 线程池参数

        • 核心与最大线程数一样
        • 阻塞队列LinkedBlockingQueue,无界阻塞队列
        • 默认的拒绝策略AbortPolicy()
      • 使用场景:

    • newWorkStealingPool(JDK.1.8出现)

      • 创建一个维护足够的线程以支持给定的并行级别的线程池,并且可以使用多个队列来减少争用,底层调用ForkJoinPool来创建线程池
    • newCachedThreadPool

      • 缓存线程池:创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程,底层调用new ThreadPoolExecutor()进行线程池的创建
    • newScheduledThreadPool

      • 定时线程池: 创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行
    • newSingleThreadExecutor

      • 单线程线程池:创建一个使用从无界队列运行的单个工作线程的执行程序。
    • newSingleThreadScheduledExecutor

      • 创建一个单线程执行器,可以调度命令在给定的延迟之后运行,或定期执行。
  • 使用ThreadPoolExecutor自定义创建线程池

    • ThreadPoolExecutor线程池实际在内部构建了一个生产者消费者模型,将线程和任务两者解耦,不直接关联,从而良好的缓冲任务,复用线程。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

    • ThreadPoolExecutor将任务提交和任务执行进行了解耦,用户只需提供Runnable对象到execute()方法后线程池完成任务的调度执行,无需关注如何创建线程和调度线程执行任务

    • new ThreadPoolExecutor() 原生线程池:当线程池运行时,有任务提交时,如果小于核心线程数时,会继续创建新的线程来继续执行任务,当达到核心线程数时,任务请求将排队进入队列,

    • 参数说明:

      • corePoolSize 核心线程数

      • maximumPoolSize 最大线程数

      • BlockingQueue workQueue 阻塞队列

        • 无界队列 :可能会导致内存溢出,当并发特别大时,大量任务积压在队列中,导致内存溢出OOM

        • 有界对列 :有助于在使用在有限的maxPoolSizes时防止资源耗尽,但可能更难调整和控制。使用大队列和小型池可以最大限度地减少CPU使用率,OS资源和上下文切换开销,但可能导致人为的低吞吐量。 如果任务频繁阻塞(例如,如果它们是I/O绑定),则系统可能能够安排比您允许的更多线程的时间。 使用小型队列通常需要较大的池大小,这样可以使CPU繁忙,但可能会遇到不可接受的调度开销,这也降低了吞吐量。

          • : 不存储元素的阻塞队列
      • RejectedExecutionHandler 四种拒绝策略

        • AbortPolicy() 线程池默认拒绝策略 丢弃任务,并抛出异常
        • DiscardPolicy() 静默丢弃任务
        • DiscardOldestPolicy() 丢弃队列最前面的任务,然后重新提交被拒绝的任务。
        • CallerRunsPolicy 由调用线程处理该业务
        • 可以自己实现RejectedExecutionHandler接口,自定义拒绝策略
      • ThreadFactory 线程创建工厂 默认线程工厂Executors.defaultThreadFactory()它创建所有线程与所有相同的ThreadGroup并且具有相同的优先级和非守护进程状态NORM_PRIORITY

      • keepAliveTime 线程空闲时间 。默认为非核心线程,设置核心线程空闲时间得方法是allowCoreThreadTimeOut(boolean)

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
//获取线程池的运行状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }
//获取线程池中工作线程的数量
private static int workerCountOf(int c)  { return c & CAPACITY; }
//通过线程数量和状态生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
  • 常见的阻塞队列

名称描述
ArrayBlockingQueue基于数组实现的有界队列,按照FIFO原则进行排序,支持公平锁和非公平锁
LinkedBlockingQueue基于链表实现的有界队列,按照FIFO原则进行排序,默认长度Integer.MAX_VALUE
priorityBlockingQueue一个支持线程优先级排序的无界队列,默认自然序(字典排序)进行排列,也可以自定义时间compareTo()方法来指定排序规则,不能保证同优先级元素的顺序
DelayQueue底层使用的是PriorityQueue队列来实现优先级的无界队列,PriorityQueue队列在添加元素的时候使用了siftUpComparable方法,特点:支持延时获取
SynchronousQueue不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁 。newCachedThreadPool()中就使用了该队列,新任务到来有空闲线程会复用,超过60后会被回收
LinkedTransferQueue基于链表实现的无界阻塞队列,比其他队列多了tryTransfer、transfer 方法
LinkedBlockingDeque基于链表的双向阻塞队列,队头与队尾都可以添加和移除元素,多线程并发时,可以最多将锁的竞争程度降低到一半
  • 二进制数据在内存中是以补码的方式进行存储的(原码,反码,补码),正数的原,反,补都相同,负数的补码=反码+1

  • ThreadPoolExecutor的生命周期

    • ThreadPoolExecutor的运行状态有5种,ctl变量保存线程池运行状态和线程池有效线程数量,高3位保存运行状态(runState),低29位保存有效线程数量(workerCount)采用AtomicInteger类型,不必维护两者的一致性,而且内部提供了计算生命周期状态和线程池线程数量

      • RUNNING : 能接收新提交的任务,并且也能处理阻塞队列中的任务
      • SHUTDOWN :关闭状态,不能接收新提交的任务,但可以继续处理阻塞队列中已保存的任务
      • STOP :不能接收新提交的任务,也不处理队列中的任务,会中断正在处理的任务的线程
      • TIDYING :所有任务都已终止,workerCount(有效线程数)为0
      • TERMINATED : 在terminated()方法执行后进入该状态

  • 线程池任务执行机制

    • 线程池任务分配流程

      • 1.检测线程池状态,若不是RUNNING,则直接拒绝提交的任务
      • 2.判断线程数是否 < 核心线程数(corePoolSize),是则创建启动一个线程来执行新提交的任务
      • 3.若线程数是否 >= 核心线程数(corePoolSize),且池内的阻塞队列未满,则将任务添加到阻塞队列中
      • 4.若线程数大于等于核心线程数(corePoolSize)并且线程数小于最大线程数,且队列已满,则创建一个线程来执行新提交的任务
      • 5.若线程数大于等于最大线程数,且队列已满,则根据拒绝策略来处理该任务
    • 线程池任务申请流程

      • 任务提交后有两种执行的可能,一种是新提交任务直接执行,还有一种就是从阻塞队列中获取任务进行执行,线程池执行入口在execute()方法中,可以接收一个Runnable对象
      • 当线程数小于核心线程数时,则创建一个线程来执行新提交的任务,当线程数大于等于核心线程数且队列已满,则创建一个线程来执行队列中的任务,所以任务可以由新创建的线程执行,或者可以被空闲线程执行
    • Worker线程管理

      • Worker类,实现了Runnable对象继承了AbstractQueuedSynchronizer类,并重写了run()方法,所以当创建Worker工作线程时,firstTask属性可能为空(空时则到阻塞队列获取执行任务)
      • ThreadPoolExecutor.execute()方法执行时通过线程池的判断执行addWorker(command, true);addWorker(null, false);addWorker(command, false),方法来增加Worker线程,当增加成功后会启动Worker线程,start方法,异步执行run方法(已经被重新实际执行(runWorker()方法调用同步Runnable对象的run方法))后;
      • runWorker方法中,会根据firstTask是否为空,while (task != null || (task = getTask()) != null),空时则从阻塞队列来获取执行任务。processWorkerExit(w, completedAbruptly);当获取不到任务时,自己会主动完成线程回收
      • Worker线程销毁依赖JVM自动回收,线程池做的工作是根据当前线程池状态,维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些需要回收时,只需要消除其引用即可
  • 线程池中的锁

    • private final ReentrantLock mainLock = new ReentrantLock();

      • ReentrantLock是一个可重入的独占锁,可重入在于同一个线程可以重复持有锁,线程池中mainLock主要是和Worker集合使用的,由于HashSet是线程不安全的,所以需要加锁, 当线程池关闭时,需要对线程池中的线程中断,mainLock就避免了线程中断风暴。由于还有一些其他参数需要统计,所以就不使用线程安全的集合类了 而是采用加锁的方案,
    • Worker类中的锁

      • Worker类继承了AbstractQueuedSynchronizer抽象类,主要是来维护工作线程的状态,保证正在执行任务的线程不能被中断, 简单的实现了加锁和释放锁,state为1时已上锁,0时未上锁,初始为-1,是不可重入的独占锁(防止在变更核心线程数时setCorePoolSize()方法是,可重入锁有可能将正在执行任务的线程中断)

参考资料:tech.meituan.com/2018/01/19/…