tomcat线程池总结
一.tomcat线程池
在开发中我们经常会碰到“池”的概念,比如数据库连接池、内存池、线程池、常量池等。为什么需 要“池”呢?程序运行的本质,就是通过使用系统资源(CPU、内存、网络、磁盘等)来完成信息的处理, 比如在JVM中创建一个对象实例需要消耗CPU和内存资源,如果你的程序需要频繁创建大量的对象,并且这 些对象的存活时间短,就意味着需要进行频繁销毁,那么很有可能这部分代码会成为性能的瓶颈。 而“池”就是用来解决这个问题的,简单来说,对象池就是把用过的对象保存起来,等下一次需要这种对象 的时候,直接从对象池中拿出来重复使用,避免频繁地创建和销毁。在Java中万物皆对象,线程也是一个对 象,Java线程是对操作系统线程的封装,创建Java线程也需要消耗系统资源,因此就有了线程池。JDK中提 供了线程池的默认实现,我们也可以通过扩展Java原生线程池来实现自己的线程池。 同样,为了提高处理能力和并发度,Web容器一般会把处理请求的工作放到线程池里来执行,Tomcat扩展 了原生的Java线程池,来满足Web容器高并发的需求,下面我们就来学习一下Java线程池的原理,以及 Tomcat是如何扩展Java线程池的。
jdk提供的线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
执行流程:
每次提交任务时,如果线程数还没达到核心线程数corePoolSize,线程池就创建新线程来执行。当线程数达 到corePoolSize后,新增的任务就放到工作队列workQueue里,而线程池中的线程则努力地从workQueue 里拉活来干,也就是调用poll方法来获取任务。 如果任务很多,并且workQueue是个有界队列,队列可能会满,此时线程池就会紧急创建新的临时线程来 救场,如果总的线程数达到了最大线程数maximumPoolSize,则不能再创建新的临时线程了,转而执行拒 绝策略handler,比如抛出异常或者由调用者线程来执行任务等。 如果高峰过去了,线程池比较闲了怎么办?临时线程使用poll(keepAliveTime, unit)方法从工作队列中拉 活干,请注意poll方法设置了超时时间,如果超时了仍然两手空空没拉到活,表明它太闲了,这个线程会被 销毁回收。
Java提供了一些默认的线程池实现,比如FixedThreadPool和CachedThreadPool,它们的本质就是给 ThreadPoolExecutor设置了不同的参数,是定制版的ThreadPoolExecutor。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
从上面的代码你可以看到:
fixedThreadPool有有固定长度(nThreads)的线程数组,忙不过来时会把任务放到无限长的队列里,这是 因为LinkedBlockingQueue默认是一个无界队列。
cachedThreadPool的maximumPoolSize参数值是Integer.MAX_VALUE,因此它对线程个数不做限制, 忙不过来时无限创建临时线程,闲下来时再回收。它的任务队列是SynchronousQueue,表明队列长度为 0。
为什么jdk提供了线程池,tomcat还要自定义线程池,是因为jdk提供的线程池是cpu类型的(cpu计算类型的任务处理较快,处理完了可以去queue再取task),而tomcat 处理的请求大多数io相关的,如果核心线程满了,就入队,那io请求就会被阻塞,所以tomcat是线程个数到最大线程数之后,才会进队列,这个和jdk的有点区别。
Tomcat线程池扩展了原生的ThreadPoolExecutor,通过重写execute方法实现了自己的任务处理逻辑:
- 1.前corePoolSize个任务时,来一个任务就创建一个核心线程
- 2.再来任务的话,如果总线程数未达到maximumPoolSize就创建工作线程,直到等于maximumPoolSize,否则将任务放到任务队列中
- 3. 如果队列满了,执行拒绝策略
tomcat线程池参数
- maxConnections: tomcat最大连接数
- maxThreads: 每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务容器可以同时处理多少个请求。maxThreads默认200,肯定建议增加。但是,增加线程是有成本的,更多的线程,不仅仅会带来更多的线程上下文切换成本,而且意味着带来更多的内存消耗。JVM中默认情况下在创建新线程时会分配大小为1M的线程栈,所以,更多的线程异味着需要更多的内存。线程数的经验值为:1核2g内存为200,线程数经验值200;4核8g内存,线程数经验值800。
- maxSpareThreads: 最大空闲线程数,在最大空闲时间内活跃过,但现在处于空闲,若空闲时间大于最大空闲时 间,则回收,小于则继续存活,等待被调度。
- minSpareThreads: 最小空闲线程数,无论如何都会存活的最小线程数
- acceptCount: 当调用HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数,默认100。如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。
- maxIdleTime: 最大空闲时间,超过这个空闲时间,且线程数大于最小空闲数的,都会被回收
- maxQueueSize: 线程池TaskQueue的长度,默认是Integer.Max_Value
- prestartminSpareThreads: boolean类型,是否线程池启动的时候就创建minSpareThreads个线程数,默认false
小结
这里面最核心的就是如何确定maxThreads的值,如果这个参数设置小了,Tomcat会发生线程饥饿,并且请求的处理会在队列中排队等待,导致响应时间变长;如果maxThreads参数值过大,同样也会有问题,因为 服务器的CPU的核数有限,线程数太多会导致线程在CPU上来回切换,耗费大量的切换开销。
有很多公式,这里就简单说下,实际场景是需要压力测试,根据场景动态的设置参数值
线程池中其他的参数,最好就用默认值,能不改就不改,除非在压测的过程发现了瓶颈。如果发现了问题就 需要调整,比如maxQueueSize,如果大量任务来不及处理都堆积在maxQueueSize中,会导致内存耗尽, 这个时候就需要给maxQueueSize设一个限制。当然,这是一个比较极端的情况了。 再比如minSpareThreads参数,默认是25个线程,如果你发现系统在闲的时候用不到25个线程,就可以调 小一点;如果系统在大部分时间都比较忙,线程池中的线程总是远远多于25个,这个时候你就可以把这个 参数调大一点,因为这样线程池就不需要反复地创建和销毁线程了。
二.Tomcat网络参数
接下来我们看看Tomcat两个⽐较关键的参数:maxConnections和acceptCount。在解释这个参数之前,先简单回顾下TCP连接的建⽴过程:客⼾端向服务端发送SYN包,服务端回复SYN+ACK,同时将这个处于SYN_RECV状态的连接保存到半连接队列。客⼾端返回ACK包完成三次握⼿,服务端将ESTABLISHED状态的连接移⼊accept队列,等待应⽤程序(Tomcat)调⽤accept(每次调用socket.accept都会将该连接从队列头中移出)⽅法将连接取⾛。这⾥涉及两个队列:
半连接队列:保存SYN_RECV状态的连接。队列⻓度由net.ipv4.tcp_max_syn_backlog设置。
accept队列(全连接队列):保存ESTABLISHED状态的连接。队列⻓度为min(net.core.somaxconn,backlog)。其中backlog是我们创建ServerSocket时指定的参数,最终会传递给listen⽅法:
int listen(intsockfd,intbacklog); //服务器new SocketServer的时候,可以传这个参数 public ServerSocket(int port, int backlog) throws IOException { this(port, backlog, null); }
你可以想象在⾼并发情况下当Tomcat来不及处理新的连接时,这些连接都被堆积在accept队列中,⽽acceptCount参数可以控制accept队列的⻓度,超过这个⻓度时,内核会向客⼾端发送RST,这样客⼾端会触发上⽂提到的“Connectionreset”异常。
⽽Tomcat中的maxConnectionss是指Tomcat在任意时刻接收和处理的最⼤连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会再从accept队列中取⾛连接,这时accept队列中的连接会越积越多。
maxConnections的默认值与连接器类型有关:NIO的默认值是10000,APR默认是8192。
所以你会发现Tomcat的最⼤并发连接数等于maxConnectionss+acceptCount。如果acceptCount设置得过⼤,请求等待时间会⽐较⻓;如果acceptCount设置过⼩,⾼并发情况下,客⼾端会⽴即触发Connectionreset异常。
三.Tomcat的高效配置:
tomcat: uri-encoding: UTF-8 #最大工作线程数,默认200, 4核8g内存:大概可抗500-1000并发,线程数经验800 #操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。 max-threads: 1000 # 等待队列长度,默认100 accept-count: 1000 max-connections: 20000 # 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量 min-spare-threads: 100
四. 图解:maxConnections、maxThreads、acceptCount关系
用一个形象的比喻,通俗易懂的解释一下tomcat的最大线程数(maxThreads)、最大等待数(acceptCount)和最大连接数(maxConnections)三者之间的关系。
我们可以把tomcat比做一个火锅店,流程是取号、入座、叫服务员,可以做一下三个形象的类比:
(1)acceptCount 最大等待数
可以类比为火锅店的排号处能够容纳排号的最大数量;排号的数量不是无限制的,火锅店的排号到了一定数据量之后,服务往往会说:已经客满。
(2)maxConnections 最大连接数
可以类比为火锅店的大堂的餐桌数量,也就是可以就餐的桌数。如果所有的桌子都已经坐满,则表示餐厅已满,已经达到了服务的数量上线,不能再有顾客进入餐厅了。
(3)maxThreads:最大线程数
可以类比为厨师的个数。每一个厨师,在同一时刻,只能给一张餐桌炒菜,就像极了JVM中的一条线程。
整个就餐的流程,大致如下:
(1)取号:如果maxConnections连接数没有满,就不需要取号,因为还有空余的餐桌,直接被大堂服务员领上餐桌,点菜就餐即可。如果 maxConnections 连接数满了,但是取号人数没有达到 acceptCount,则取号成功。如果取号人数已达到acceptCount,则拿号失败,会得到Tomcat的Connection refused connect 的回复信息。
(2)上桌:如果有餐桌空出来了,表示maxConnections连接数没有满,排队的人,可以进入大堂上桌就餐。
(3)就餐:就餐需要厨师炒菜。厨师的数量,比顾客的数量,肯定会少一些。一个厨师一定需要给多张餐桌炒菜,如果就餐的人越多,厨师也会忙不过来。这时候就可以增加厨师,一增加到上限maxThreads的值,如果还是不够,只能是拖慢每一张餐桌的上菜速度,这种情况,就是大家常见的“上一道菜吃光了,下一道菜还没有上”尴尬场景。
总结: