Tomcat如何扩展Java线程池

125 阅读5分钟

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线程池处理逻辑:

  1. 前corePoolSize个任务,来一个任务就创建一个新线程。
  2. 后面再来任务,就把任务添加到任务队列,让所有线程去抢,如果队列满了就创建临时线程。
  3. 如果总线程达到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方法实现自己的逻辑:

  1. 前corePoolSize个任务,来一个任务就创建一个新线程。

  2. 后面再来任务,就把任务添加到任务队列,让所有线程去抢,如果队列满了就创建临时线程。

  3. 如果总线程数达到了maximumPoolSize,则继续尝试把任务添加到任务队列。

  4. 如果再次添加队列也失败,执行拒绝策略。

//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,整个线程池的逻辑如下:

  1. 前corePoolSize个任务,来一个任务就创建一个新线程。
  2. 后面再来任务,就把任务添加到任务队列,让所有线程去抢。如果已提交的任务大于当前线程数量,则会创建临时线程,直到线程数达到最大线程。
  3. 如果队列满了(达到了最大线程数量后,继续添加任务到队列导致),执行了拒绝策略。则继续尝试把任务添加到任务队列。
  4. 如果再次添加队列也失败,执行拒绝策略。

3. 参考资料

  1. 《深入拆解Tomcat & Jetty》- 极客时间
  2. Tomcat源码分支8.5.x