Tomcat如何扩展Java线程池
1. Java线程池
Java线程池的创建参数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
每次提交任务时,如果线程数还没达到corePoolSize,线程池就创建新线程来执行。达到corePoolSize之后,新增的任务就放到工作队列workQueue中,而线程池的线程则努力从workQueue拉取处理任务。
如果任务很多,并且workQueue是个有界队列,队列可能会满,此时线程池就会紧创建新的临时线程来救场,如果总的线程达到了最大线程数maximumPoolSize,则不能再创建线程了,转而执行拒绝策略handler。
如果高峰期过去,线程池比较空闲,临时线程使用poll(keepAliveTime,unit)方法从工作队列中拉去不到任务,则线程就会被回收。
threadFactory可以扩展原生的线程工厂,如给创建的线程重新命名。
Java线程池处理逻辑:
- 前corePoolSize个任务,来一个任务就创建一个新线程。
- 后面再来任务,就把任务添加到任务队列,让所有线程去抢,如果队列满了就创建临时线程。
- 如果总线程达到maximumPoolSize,执行拒绝策略。
2. Tomcat线程池
2.1 定制版ThreadPoolExecutor
Tomcat的线程池也是一个定制版本的ThreadPoolExecutor,但是Tomcat需要限制线程个数、队列长度,否则CPU和内存存在有资源耗尽的风险。Tomcat构建线程池代码如下:
//AbstractEndpoint#createExecutor
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
- Tomcat定制了任务队列和线程工厂。
- Tomcat对线程数也有限制,设置了核心线程数(minSpareThreads)和最大线程池数量(maxThreads)。
除资源限制外,Tomcat线程池还定制自己的任务处理流程。
Tomcat线程池扩展了原生的ThreadPoolExecutor,通过重写execute方法实现自己的逻辑:
-
前corePoolSize个任务,来一个任务就创建一个新线程。
-
后面再来任务,就把任务添加到任务队列,让所有线程去抢,如果队列满了就创建临时线程。
-
如果总线程数达到了maximumPoolSize,则继续尝试把任务添加到任务队列。
-
如果再次添加队列也失败,执行拒绝策略。
//Tomcat 8.5.x版本,其他版本可能有差别
//ThreadPoolExecutor#execute
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
//内部逻辑和Java 原生线程池的类似
executeInternal(command);
} catch (RejectedExecutionException rx) {
if (getQueue() instanceof TaskQueue) {
//如果线程总数大达到maximumPoolSize,就会触发拒绝策略
final TaskQueue queue = (TaskQueue) getQueue();
try {
//继续尝试把任务放到任务队列中去
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;
}
}
}
Java原生线程池的execute方法执行任务,如果总线程数达到maximumPoolSize,就会抛出RejectedExecutionException异常。但这个异常被Tomcat线程池的execute方法捕获,判断队列是否是TaskQueue。如果是的话,继续尝试把这个任务放到任务队列中,如果任务队列还满的话,再抛出异常。这样做的原因是:第一次尝试放队列是满的,失败,再尝试创建临时线程,也满了。但这个过程中,队列的任务可能被其他线程消费了一部分,再往队列添加有可能会成功。
2.2定制版任务队列
在Tomcat线程池execute方法执行前有这么一行
submittedCount.incrementAndGet();
再执行拒绝策略或者任务完成时,会执行下面的语句
submittedCount.decrementAndGet();
Tomcat线程池用submittedCount来维护已经提交到线程池,但还没有执行完的任务个数。维护这个变量,和定制版的任务队列有关。
Tomcat定制版的任务队列是TaskQueue,扩展了Java的LinkedBlockingQueue。LinkedBlockingQueue默认是个无界队列,也可以通过capacity设置队列长度。Tomcat中可以用maxQueueSize参数来设置队列长度,单默认情况下为Integer.MAX_VALUE,等于没有限制。这样会有个问题:当线程数达到核心线程数量后,再来任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会创建新线程了。
为了解决该问题,TaskQueue重写了LinkedBlockingQueue的offer方法(往队列添加任务用offer方法),在合适的时机返回false,表示添加任务失败,这是线程池就会创建新的线程。offer方法核心逻辑如下:
//TaskQueue#offer
public Boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) {
return super.offer(o);
}
//如果线程数量已经到了最大值,不能创建新线程了,只能把任务添加对任务队列。
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;
}
//默认情况把任务添加到队列
return super.offer(o);
}
从代码中可以看出:只有当前线程数大于核心线程数、小于最大线程数,并且已提交的任务个数大于当前线程数时,也就是说线程不够用了,但是线程数又没达到极限,才会去创建新的线程。这也是Tomcat需要维护已提交任务数这个变量,目的是在任务队列长度无限制的情况下,让线程池有机会创建新的线程。
2.3总结
定制版本的ThreadPoolExecuter和TaskQueue,整个线程池的逻辑如下:
- 前corePoolSize个任务,来一个任务就创建一个新线程。
- 后面再来任务,就把任务添加到任务队列,让所有线程去抢。如果已提交的任务大于当前线程数量,则会创建临时线程,直到线程数达到最大线程。
- 如果队列满了(达到了最大线程数量后,继续添加任务到队列导致),执行了拒绝策略。则继续尝试把任务添加到任务队列。
- 如果再次添加队列也失败,执行拒绝策略。
3. 参考资料
- 《深入拆解Tomcat & Jetty》- 极客时间
- Tomcat源码分支8.5.x