线程池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()方法是,可重入锁有可能将正在执行任务的线程中断)
-