池化技术
池化技术是一种常用的提升性能的手段,比如常见的数据库连接池、JAVA字符串的常量池、以及线程池等。
以JAVA中的线程为例,创建一个新线程的背后,其实是调用操作系统的api,消耗宝贵的系统资源,去执行一些业务逻辑。
如果我们频繁的创建销毁线程对象,对于CPU、内存都是一个很大的负担。
所以,线程池就应运而生了。本质上,池化技术是一种以空间换时间的做法。
线程池就是提前创建好一些线程供我们使用,使用完毕之后并不销毁这些线程,而是放回池中,等待下次继续使用。
而tomcat作为web界的容器一哥,自然也使用了线程池这种优化手段,为了提高业务处理能力和支持更高的并发,tomcat还对线程池做了更进一步的优化。
原生线程池
讲到tomcat优化线程池之前,我们先回顾一下JAVA原生的线程池。
JAVA原生线程池ThreadPoolExecutor位于java.util.concurrent包下。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
可以看到,ThreadPoolExecutor提供了7个参数。
其工作原理大致如下:
1,当线程池收到一个新任务,先判断当前线程数是否大于corePoolSize,如果小于corePoolSize,就新建一个线程执行任务。
2,当线程池中的线程数已经达到corePoolSize,再来新的任务就不会新建线程了,而是将任务投递到workQueue中。当核心线程有空闲了,就会从workQueue队列poll任务去执行。
3,如果任务非常多,workQueue已经达到最大任务数量了。这时maximumPoolSize就会发挥作用了。线程池会继续创建新线程用于执行任务,直至最大线程数达到maximumPoolSize。
4,如果任务继续增加,所有线程都满负荷工作了,队列也满了。此时线程池就会开始执行拒绝策略了,就是我们定义的handler。
5,当任务高峰期过去了,临时线程闲置下来了,此时线程池就会从workQueue中拉取任务继续执行了。临时线程使用poll(keepAliveTime, unit)方法拉取任务,如果在指定的unit时间内未获取到任务,临时线程就会被销毁回收。
除了默认的线程池以外,JAVA还提供了一些定制化的线程池,比如FixedThreadPool、CachedThreadPool等。
其实就是给默认的线程池设置了一些特殊的参数,以满足不同场景下的业务需要,这里就不过多介绍了,感兴趣的朋友可以通过源码去了解一下。
tomcat的线程池
通过对JAVA原生线程池的介绍,我们可以知道,当达到最大线程数时,表示线程池已经满负荷运行,无法再接收任务了。此时会执行线程池的拒绝策略,比如抛出异常、丢弃任务等。
那tomcat做了什么优化手段来提升并发量呢?
其实原理很简单:当总线程数达到最大线程maximumPoolSize时,不是立刻执行拒绝策略。而是先尝试将任务投递到任务队列中,如果任务队列此时仍然是满的,再执行拒绝策略。
如上图所示,就是tomcat线程池的execute方法。
如果我们点进executeInternal方法,就会发现其实现代码和JAVA原生线程池的execute方法是一样的,tomcat只是将原生执行方法包装了一层。
tomcat在外层catch了RejectedExecutionException异常,当异常抛出时,表示任务已满需要执行拒绝策略了。此时tomcat尝试将任务再次投递到任务队列,如果投递失败,再抛出一次RejectedExecutionException异常,转而去执行拒绝策略。
看到这,有的朋友可能会问了:任务都满了,再投递一次任务到队列中有什么用呢?
举个例子大家就明白了。
比如一个平稳运行的系统忽然遇到大量的流量涌入,但是这些请求可能大部分都是一些简单的CPU密集型任务,比如简单的计算、查询,并非耗时较长的IO任务。
一开始,tomcat默认的200个线程和10000的任务队列可能瞬间就被打满了,下一个任务进来时,由于线程池已经满负荷运行,可能就需要执行拒绝策略。
但是!这些简单的请求耗时是非常短的,可能几毫秒就已经任务完成。此时,任务队列已经有了可继续投递的剩余空间。
那么简单的一次继续投递任务队列的尝试,可能就会使本该抛异常的请求继续执行。那么反应到应用层面,就是服务器能继续正常处理请求,从而提升了服务器的并发处理能力。
另外,大家可以在execute方法中看到第一行的:submittedCount.incrementAndGet();
默认情况下,tomcat的任务队列TaskQueue的capacity是Integer.MAX_VALUE。
这样的话,当线程数达到核心线程数以后,再来新的任务都会被投递到任务队列中,就没有办法再创建新线程了,这样肯定是不行的。
所以,tomcat重写了LinkedBlockingQueue的offer方法,如果当前提交的任务数submittedCount大于核心线程数,并且小于最大线程数的情况下,就去创建一个新线程。
这样,就避免了无限往队列中投递任务的情况。
总结一下就是,对于一个新任务,有空闲的线程就先用空闲的线程,线程不够用又没达到上限就去创建新线程,线程达到上限就扔到队列排队去,队列满了执行重试机制,重试失败那就没办法了,执行拒绝策略吧。
最后
对于一个web容器来说,首先要考虑的自然就是性能,而性能指标又是通过什么来决定呢?
CPU和内存。
我们知道,每创建一个线程都是要消耗内存的(大约1M左右),如果无限制的创建线程势必会造成内存的浪费。
而对于CPU密集型的任务来说,过多的线程会造成CPU频繁的上下文切换,反而会降低服务器的性能。
所以,线程数并非是越大越好,应该根据具体的业务进行压测从而配置适当的参数。
在Springboot中,可以通过server.tomcat.max-threads/min-spare-threads/max-connections来修改线程池的默认配置。
finally,感谢您的点赞和关注。