前言
我们知道,池化概念,是一种非常常见的资源优化手段,池化主要就是针对会被重复使用的某些资源进行管理,避免每次创建以及销毁而带来较高的使用代价。
一、Java中的线程池
核心参数
首先我们要先搞清楚线程池中一些关键参数的含义
corePoolSize:核心线程数,即线程池始终保持着corePoolSize个线程数。
maximumPoolSize:线程池中最多允许创建maximumPoolSize个线程。
keepAliveTime:假设corePoolSize是5,maximumPoolSize是6,因此有1个线程是非核心线程,那么这个非核心线程就会在空闲了
keepAliveTime时间后被销毁。
workQueue:这是一个阻塞队列,用于存放当前线程来不及处理的任务。
threadFactory:创建线程的工厂,为每个线程起一个有意思的名称,方便问题排查。
handler:拒绝策略,定义如果阻塞队列被放满了以后,接下来的任务如何处理。
线程池使用注意
为了方便使用,Java中提供了一些已经封装好的线程池,但他们都或多或少存在一些问题,比如:FixedThreadPool,由于其存放任务的队列,几乎是无限大的,因此就有可能造成大量的请求堆积,最终导致OOM发生。
关于这一点在阿里开发手册中,也有提及:
总体对比
二、Tomcat中的线程池
毫无疑问,线程池中的线程数以及队列大小是最重要的两个关键参数, 那我们就来看看Tomcat是如何选择的。
下面是Tomcat中创建一个线程池执行器对象的方式,我们主要针对参数进行一些了解
// 定制队列
taskqueue = new TaskQueue(maxQueueSize);
// 定制线程工厂
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
1. getMinSpareThreads,获取核心线程数,默认值为25
/**
* min number of threads
*/
protected int minSpareThreads = 25;
public int getMinSpareThreads() {
return minSpareThreads;
}
2. getMaxThreads,获取最大线程数,默认值200
/**
* max number of threads
*/
protected int maxThreads = 200;
public int getMaxThreads() {
return maxThreads;
}
3. maxIdleTime,对应的就是keepAliveTime,线程闲置时间
/**
* idle time in milliseconds
*/
protected int maxIdleTime = 60000;
最后一个线程工厂,也就是TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
4. namePrefix
protected String namePrefix = "tomcat-exec-";
5. daemon
protected boolean daemon = true;
6. getThreadPriority,默认优先级:Thread.NORM_PRIORITY
protected int threadPriority = Thread.NORM_PRIORITY;
public int getThreadPriority() {
return threadPriority;
}
7. 现在还有一个最要的参数,就是taskqueue,Tomcat并没有直接使用JDK中的队列,而是自己重新定制一个。
我们先注意观察,taskqueue队列构建时,传入的参数taskqueue = new TaskQueue(maxQueueSize);
protected int maxQueueSize = Integer.MAX_VALUE;
可以看到,这也是一个几乎无限大的队列数,前面我们说过,应当避免构建无限大的队列,主要原因有两点,第一,可能会造成内存溢出,第二,最大线程数的设置将变的没有意义,关于第一点虽然的确存在内存溢出的风险,但实际上在Tomcat服务中一般到不会,因为Tomcat中的线程池主要就是处理HTTP请求,没有哪个系统能让一个请求到Tomcat上的HTTP请求积压到内存溢出。
常规执行流程
因此我们只要来看看Tomcat是如何解决第二个问题的?再这之前,我们应该先清楚一个调用流程
只有在当前活跃线程数大于核心线程数,且任务队列已满,且当前活跃线程数小于最大线程数时,才会构建新的线程。
Tomcat中的特殊处理
基本流程清楚后,我们再额外看一下Tomcat中的一些特殊处理。
Tomcat中的线程池有一个额外的属性submittedCount
,你可以简单的理解为就是一个计数器,每当有一个任务执行在执行时submittedCount
就会加1,当任务执行完成后submittedCount
就会减1,具体记录这个数字有什么作用,继续往下看就知道了。
private final AtomicInteger submittedCount = new AtomicInteger(0);
public void execute(Runnable command, long timeout, TimeUnit unit) {
//
submittedCount.incrementAndGet();
try {
executeInternal(command);
} catch (RejectedExecutionException rx) {
if (getQueue() instanceof TaskQueue) {
// If the Executor is close to maximum pool size, concurrent
// calls to execute() may result (due to Tomcat's use of
// TaskQueue) in some tasks being rejected rather than queued.
// If this happens, add them to the queue.
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;
}
}
}
现在,我们再来看看TaskQueue
队列到底做了什么,首先TaskQueue
继承了LinkedBlockingQueue
,并且实际上也没有修改什么,只是offer
方法做了一些额外的处理。
public class TaskQueue extends LinkedBlockingQueue<Runnable> {}
前面我们看到的getSubmittedCount
的作用在offer
方法就使用上了。
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) {
return super.offer(o);
}
//we are maxed out on threads, simply queue the object
// 如果当前活跃的线程数等于最大线程数,那么就不能创建线程了,因此直接放入队列中
if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
return super.offer(o);
}
// 如果执行到这,说明当前线程数大于核心线程数,且小于最大线程数,因此正常情况下会根据队列任务数来判定是否可以继续创建新线程,但tomcat中并不是这样的,其逻辑就在下面的几行代码中。
//we have idle threads, just add it to the queue
// 如果当前提交的任务数小于等于当前活跃的线程数,表示还有空闲线程,直接添加到队列,让线程去执行即可。
if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
return super.offer(o);
}
//if we have less threads than maximum force creation of a new thread
// 走到这说明当前提交的线程数大于当前活跃的线程数。
// 因此再校验下当前活跃线程数是否小于最大线程数,如果小于,此时就可以创建新的线程了。
if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
return false;
}
//if we reached here, we need to add it to the queue
return super.offer(o);
}
因此Tomcat的线程池策略是,虽然任务队列是无限大的,但依然能够保证有机会创建新的线程。
Tomcat中的执行流程
总结
Tomcat结合了自身应用场景的特点,使用了无限大的队列来承载任务,做到了尽量不干预业务本身,保证了请求一定会交给业务服务去处理,但同时又为了解决无限大队列后,无法创建新的线程的问题,Tomcat又重写了Java提供的ThreadPoolExecutor
类中的execute
方法,以及定制了TaskQueue
任务队列,并主要修改了offer
方法,使得在拥有无限大的任务队列的同时,也能有机会创建新的线程。