这次我真的忘不了你了
献上标准线程池Api
public ThreadPoolExecutor(int corePoolSize,//核心数
int maximumPoolSize,//最大线程数
long keepAliveTime,//空闲时间
TimeUnit unit,//单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略
) {//省略
}
整体模型
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。
线程池的运行主要分成两部分:
- 任务管理
- 充当生产者的角色,当任务提交后,线程池会判断该任务后续的三种走向:
- 直接申请线程执行该任务(如果线程数
workerCount
小于核心数) - 缓冲到队列中等待线程执行(如果)
- 拒绝该任务
- 直接申请线程执行该任务(如果线程数
- 充当生产者的角色,当任务提交后,线程池会判断该任务后续的三种走向:
- 线程管理
- 线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
生命周期
ThreadPoolExecutor的运行状态有5种,分别为:
其生命周期转换如下入所示:
线程池内部使用一个变量维护两个值:运行状态(runState)和线程池内有效线程的数量 (workerCount)
用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
任务执行机制
当我们execute方法执行时,任务是怎么被调度的呢?
任务调度
- 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
执行流程如下图所示:
这里重点是,无界队列由于阻塞队列一般都不会满,所以不会走到“线程数小于最大线程数”这一步的判断,导致maximumPoolSize参数无效。
阻塞队列
重要概念
Worker
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker
线程池执行线程的基本元素,是一个Runnable
增加线程 addWorker
增加线程是通过线程池中的addWorker方法
addWorker方法有两个参数:firstTask、core
-
firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空
-
core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize
重写Runnable run()
在Worker类中的run方法调用了runWorker方法来执行任务
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
try {
while (task != null || (task = getTask()) != null) {
//省略执行代码
}
} finally {
processWorkerExit(w, completedAbruptly);
}
}
执行过程如下:
- while循环不断地通过getTask()方法获取任务。
- 执行任务
- 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
getTask()
从阻塞队列中取任务
主要有两个重点
- 当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务
- 核心线程可以无限等待获取任务,对于超过核心线程数量的这些线程,需要进行超时控制。目的是控制线程池的有效线程数量
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
//第一个重点
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 对于超过核心线程数量的这些线程,需要进行超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
}
}
Worker线程回收processWorkerExit()
当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。
AQS在线程池的应用
Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。
线程回收时如何判断线程是否在运行?
Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
- lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
- 如果正在执行任务,则不应该中断线程。
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
- 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
线程池都有哪几种工作队列
-
ArrayBlockingQueue
一个用数组实现的有界阻塞队列,按FIFO排序量
-
LinkedBlockingQueue
基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE
吞吐量通常要高于ArrayBlockingQuene
-
DelayQueue
支持延时获取元素的无界阻塞队列,队列使用PriorityQueue(非concurrent包)来实现
-
PriorityBlockingQueue
一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现
compareTo()
方法来指定元素排序规则,或者初始化PriorityBlockingQueue
时,指定构造参数Comparator
来对元素进行排序 -
SynchronousQueue
(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene
重点 : offer方法如果里面有没被消费的直接返回false,put方法会阻塞知道被消费
newCachedThreadPool线程池使用了这个队列
拒绝策略
- AbortPolicy直接抛出异常阻止线程运行(默认)
- DiscardOldestPolicy移除队列最早线程尝试提交当前任务
- DiscardPolicy丢弃当前任务,不做处理
- CallerRunsPolicy如果被丢弃的线程任务未关闭,由提交这个任务的线程执行该任务
JDK提供的几种线程池
Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
设想一下,如果SynchronousQueue换成LinkedBlockingQueue,workerCountOf(recheck) == 0也会addWorker-非核心的worker,那就会永远一直放到队列中,一直被一个线程消费(这里1个coreThread和0个core是一样的)
线程池execute尝试将任务放到队列中调用SynchronousQueue.offer时,offer方法如果里面有没被消费的直接返回false,所以直接会走到addWorker-非核心的worker,所以跟无界队列不一样,他会有N个线程消费
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 核心线程数和最大线程数大小一样
- 没有所谓的非空闲时间,即keepAliveTime为0
- 阻塞队列为无界队列LinkedBlockingQueue
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
一个人(一条线程)夜以继日地干活。
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
DelayedWorkQueue :和DelayedQueue相似,但是是自己实现的
工作机制
- 添加一个任务
- 线程池中的线程从 DelayedWorkQueue 中取任务
- 线程从 DelayedWorkQueue 中获取 time 大于等于当前时间的task
- 执行完后修改这个 task 的 time 为下次被执行的时间
- 这个 task 放回DelayedWorkQueue 队列中
一些问题
线程池怎么判断线程可以回收?
线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表(HashSet<ThreadPoolExecutor.Worker> workers
)去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。重要的就是如何判断线程是否在运行
?答案是,每次runWorker都会调用AQS的lock()锁起来,当worker getTask为null时,开始尝试回收,此时tryLock,如果成功说明空闲是空闲状态则可以安全回收