本文基于 “如何解决 JDK 线程池中不超过最大线程数下即时快速消费任务,而不是在队列中堆积”这个问题,在JDK线程池的基础上做出一些调整,实现任务快速消费。
JDK线程池
工作流程
JDK线程池的执行流程如下:
①当前工作线程数 < 核心线程数corePoolSize:创建核心线程执行任务
- 核心线程长久存在,即使空闲也不会销毁,除非配置 allowCoreThreadTimeOut = true,那么核心线程就会在空闲keepAliveTime后被销毁
②当前工作线程数 >= 核心线程数:将任务加入阻塞队列
③阻塞队列已满:创建非核心线程执行任务
- 非核心线程数 + 核心线程数corePoolSize = 最大线程数maximumPoolSize
④工作线程 >= 最大线程数:执行拒绝策略,默认是抛出异常
贴源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//获取线程池信息
int c = ctl.get();
//workerCountOf(c):得到当前工作线程数
if (workerCountOf(c) < corePoolSize) { //当前工作线程数 < 核心线程数
//addWorker(command, true):尝试创建核心线程
//true为创建核心线程,false为创建非核心线程
if (addWorker(command, true))
//创建成功则结束
return;
//创建失败,再次获取线程池信息
c = ctl.get();
}
//isRunning(c):线程池是RUNNING状态,即线程池还在运行
//workQueue.offer(command):将任务加入阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
//再次获取线程池状态
int recheck = ctl.get();
//检查,如果线程池终止运行了,移除任务
if (! isRunning(recheck) && remove(command))
//执行拒绝策略
reject(command);
//线程池仍然在运行,但工作线程数为0,创建非核心线程处理队列中滞留任务
else if (workerCountOf(recheck) == 0)
//null表示处理阻塞队列中的任务,false表示创建非核心线程
addWorker(null, false);
}
//线程池终止运行,创建非核心线程处理队列中滞留任务
//或者 线程在运行,但添加任务到阻塞队列失败,即阻塞队列已满
else if (!addWorker(command, false))
//如果创建非核心线程处理任务失败,执行拒绝策略
reject(command);
}
Q:为什么将任务加入阻塞队列后,要再次获取线程池状态,进行检查?
A:因为可能任务进入阻塞队列后,线程池停止运行了,那么这个任务就会滞留在队列中,永远无法被执行,成为“僵尸任务”。
因此JDK做了双重检查,在任务成功入队后,再次检查线程池状态,如果线程池停止运行了,会将任务从队列中移除,如果移除成功,会执行拒绝策略,告知调用者,这任务我们做不了了。
但也可能存在移除任务失败的情况,如果任务已被其它线程消费,那么remove()失败,不会执行拒绝策略。
Q:线程池仍然在运行,工作线程数为0如何理解?
A:这个场景是线程池仍然是RUNNING状态,但所有线程异常终止,比如核心线程因为超时销毁(开启了allowCoreThreadTimeOut =true的配置),这时就需要创建一个非核心线程来执行掉这个任务。
再Q:为什么是创建非核心线程处理,而不是创建核心线程处理?
再A:可能是由于两者管理原则的区别,非核心线程作为弹性资源,只是短暂救急。还有一个说法是避免核心线程膨胀:如果每次无线程存活都创建核心线程,可能导致 corePoolSize 被隐性突破,违背配置约束。
到这里,应该就比较清楚JDK线程池的工作流程了。
快速消费线程池
工作流程
本文要实现的快速消费线程池,主要就是对上述流程做一个顺序调整:
①当前工作线程数 < 核心线程数:创建核心线程执行任务
②否则,创建非核心线程执行任务
③已达最大线程数,将任务加入阻塞队列
④阻塞队列满,执行拒绝策略
可以看到,就是在达到核心线程数后,接着创建非核心线程执行任务而不是把任务丢入阻塞队列,从而达到一个”快速执行任务“的效果。
代码实现
逻辑:快速消费线程池仍然是继承自JDK线程池实现ThreadPoolExecutor,新增AtomicInteger类属性 submittedTaskCount 记录当前线程池正在处理的任务数量(正在执行的任务数+队列中的任务数),我们就可以通过submittedTaskCount对比任务数和线程数,从而选择将任务放入队列给已有线程处理,还是新建线程去处理。
提交任务后,线程池执行execute()方法,核心线程数满后会执行阻塞队列的offer()方法,我们通过自定义阻塞队列,重写其中的offer()方法,让工作线程数达到核心线程数后,返回false,那么在JDK源码中,它就会去创建非核心线程执行任务。
只有工作线程数达到最大线程数时,才会将任务加入阻塞队列。
这样,就达到了我们调换顺序,提升消费任务速度的目的。
自定义任务队列
import lombok.Setter;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 快速消费任务队列
*/
public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {
@Setter
private EagerThreadPoolExecutor executor;
public TaskQueue(int capacity) {
super(capacity);
}
@Override
public boolean offer(Runnable runnable) {
int currentPoolThreadSize = executor.getPoolSize();
// 如果线程池中有线程正在空闲,将任务加入阻塞队列,由空闲线程去处理任务
if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
return super.offer(runnable);
}
//【重点】
// 当前线程池线程数量小于最大线程数,返回 False,根据线程池源码,会创建非核心线程
if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
return false;
}
// 如果当前线程池数量大于最大线程数,任务加入阻塞队列
return super.offer(runnable);
}
//重试将任务加入队列
public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
//如果线程池被关闭,直接抛出拒绝策略
if (executor.isShutdown()) {
throw new RejectedExecutionException("Executor is shutdown!");
}
return super.offer(o, timeout, unit);
}
}
快速消费线程池
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 快速消费线程池
*/
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {
public EagerThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
TaskQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
//已提交任务数,即线程池中正在处理的任务数
private final AtomicInteger submittedTaskCount = new AtomicInteger(0);
public int getSubmittedTaskCount() {
return submittedTaskCount.get();
}
/**
* 执行完任务后,submittedTaskCount-1
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
submittedTaskCount.decrementAndGet();
}
@Override
public void execute(Runnable command) {
//有任务来执行,submittedTaskCount+1
submittedTaskCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException ex) {
//按当前流程,阻塞队列满后,任务加入会被拒绝
TaskQueue taskQueue = (TaskQueue) super.getQueue();
try {
//这时再次尝试加入队列,重试机制,如果加入失败,立即拒绝
if (!taskQueue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.", ex);
}
} catch (InterruptedException iex) {
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException(iex);
}
} catch (Exception ex) {
submittedTaskCount.decrementAndGet();
throw ex;
}
}
}
此外,在队列已满,任务加入队列失败,抛出异常时,我们对其进行捕获,使用重试机制,再次尝试将任务放入队列,因为队列中的任务可能很快被消费,此时重试入队可以避免不必要的拒绝。
1万+

被折叠的 条评论
为什么被折叠?



