要知道创建线程策略是如何的,就要从构造函数入手,因为构造函数中有几个核心的参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
例如,我们用构造方法的几个参数构造了这么一个对象。
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
15,
60,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("my-%d").build(), // 这里是用了 Google Guava 的 ThreadFactoryBuilder;
new ThreadPoolExecutor.CallerRunsPolicy());
这时候我们提交100任务,这个时候线程池里的线程是如何创建的?
1 在向线程池不断提提交任务,在线程池里的工作线程的数量还不到 核心线程数量(corePoolSize)的时候,就继续新增工作线程。
2 如果工作线程数量达到了 核心线程数量(corePoolSize),还是没有停止提交任务,那么把工作线程还没来得及处理的任务增加到 中。
3 如果往 workQueue#offer 任务失败,那么就会继续创建工作线程。
比如在 ArrayBlockingList#offer的代码,返回false的情况就是 count == items.length
。
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
说明此时的队列里面任务的数量等于count
的时候,也就是满的时候,就会返回 false
,否则就会让任务入队。
4 如果此时继续新增任务,而且此时新增工作线程也失败了(也就是工作线程数量达到了最大线程数量),那么就会走拒绝的逻辑,也用 RejectedExecutionHandler
的时候了。RejectedExecutionHandler
这个接口就一个方法,还是挺容易理解的。
在Tomcat的线程改写主要在 https://github.com/apache/tomcat/blob/a801409b37294c3f3dd5590453fb9580d7e33af2/java/org/apache/tomcat/util/threads/
Tomcat 在原来的基础上做了哪些改进?Tomcat 修改了原来的创建线程的策略,原来是要在阻塞队列已经满了的情况下,才会继续增加工作线程的数量,直到达到最大线程数量。但是 tomcat 是直接把工作线程增加到了最大的工作线程的数量,然后再往阻塞队列中入队任务,这个过程的好像更加符合我们对 “最大的核心线程数” 的理解。但是 Tomcat 是紧耦合实现的功能,需要 ThreadPoolExecutor 和 TaskQueue(Tomcat 自己实现的一个阻塞队列)配合使用。
先来看看 Tomcat 是如何改造 原来JDK 里的 ThreadPoolExecutor 的。
这个线程池的执行器的执行方法,核心还是用了JDK 中的ThreadPoolExecutor,但是在异常处理的阶段做了改造。
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
// 如果队列是 TaskQueue,那么就执行 force 操作
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
// 如果 force 都失败了,那么就表示队列真满了,就抛错吧
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
下面是 Tomcat 的自定义的一个内部类 RejectHandler
private static class RejectHandler implements RejectedExecutionHandler {
private RejectHandler() {
}
public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) {
throw new RejectedExecutionException();
}
}
如果你在初始化Tomcat
的 ThreadPoolExecutor
对象的时候,没有指定 RejectedExecutionHandler 的参数,那么 Tomcat 就默认指定这个 RejectHandler 来作为拒绝策略。
尝试去用 force
任务,如果连 force
都失败了,那么就表示,队列真的满了,这个 force
方法代码如下:
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if (parent == null || parent.isShutdown()) throw new RejectedExecutionException(sm.getString("taskQueue.notRunning"));
return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected
}
可以看到,还是使用了阻塞队列的 offer
的方法,不过是带着超时时间的,预防出现长时间阻塞的情况。目的还是尽可能的把任务入队。
可以看出,Tocmat 的这个线程池的 excute
的方法其实并没有做太大的改变,还是用了原来JDK 中的 ThreadPoolExecutor 的方法,重点改造在于出现了拒绝异常之后的操作。而出现了拒绝异常之后,里面的操作的是和 TaskQueue 息息相关的。所以只有知道了 TaskQueue 的操作,才能理解 Tomcat 是如何进行修改了创建策略的。所以说,Tomcat 的实现是『紧耦合』的。
其实 TaskQueue 里面最核心的实现是就是 offer() 方法。
先来看看代码,注意,这里的parent的指的是 ThreadPoolExecutor
,某些使用的场景下,有些会去调用 setParent
方法来主动的指定。
public void setParent(ThreadPoolExecutor tp) {
parent = tp;
}
public boolean offer(Runnable o) {
//we can't do any checks 如果没有指定 ThreadPoolExecutor 为 null ,那么就直接调用父类的 offer 方法
if (parent==null) return super.offer(o);
// 如果这个时候的线程池的工作线程的数量已经达到了最大的线程数量,那么就 offer 方法,把任务offer进队列中。
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
// 如果线程池中有空闲的线程,那么把任务提交给队列
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
//如果此时的线程池里的工作线程的数量是少于线程池的最大的线程数量,那么就返回false
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
// 如果以上条件都没有触发,那么就默认 offer
return super.offer(o);
}
这里的 getSubmittedCount
返回的 submittedCount
的值,这里记录是已经提交的,但是还没执行结束的任务的数量。
private final AtomicInteger submittedCount = new AtomicInteger(0);
submittedCount
这个要和 getPoolSize()
进行比较才显得有用
offer
方法中,判断了当前线程的几种状态,工作线程是否已经达到了最大?是否有空闲线程?如果当前工作现场数量没有达到最大,那么就不进行入队操作。
Tomcat 自己实现的线程要实现的功能就是,只有在工作线程的数量达到最大的时候,才进行入队操作。
在著名开源项目 Dubbo 中 EagerThreadPoolExecutor
也是类似的实现。
还有一种松耦合的实现,下一篇玩转线程池系列,我也会继续讨论。
水平有限,写的不好的地方欢迎指出,欢迎友好交流。