1.定义
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
作用:能够极大地减少某些线程的频繁创建和销毁带来的时间开销
比如说项目中使用到了thrift,client会向server发起大量并发请求,如果每次请求都去建立一个线程,维持tcp连接,那无疑开销是巨大的,所以在客户端自己实现了一个thrift连接池。
2.线程池的实现原理
2.1 线程池的关键参数:
-
corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收
-
maximumPoolSize就是线程池中可以容纳的最大线程的数量
-
keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清 除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,
-
util,就是计算这个时间的一个单位。
-
workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
-
threadFactory,就是创建线程的线程工厂。
-
handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
2.2 线程池的常见形式
- 单线程池——newSingleThreadExecutor:线程池只维持一个活跃线程,它只会用唯一的工作线程来执行任务。
- 固定大小线程池——newFixedThreadPool:设置一个固定大小的队列,以及指定核心线程数和最大线程数,可控制线程最大并发数,超出的线程会在队列中等待,如果,队列已满但还未到最大线程数,则会继续开辟线程处理该入队失败的请求,如果已到最大线程数,则采取拒绝策略。
默认是核心线程数等于最大线程数。其阻塞队列采取链表类阻塞队列(LinkedBlockingQueue)。 - 可缓存线程池——newCachedThreadPool:核心线程数为0,最大线程数为Int.MaxInteger,阻塞队列采取同步队列(SynchronousQueue),同步队列也是阻塞队列的一种,但是其特点在于该队列每次的插入,都必须要对应一个删除操作才能继续。相当于是提交的任务不会真实保存,只会马上开启新的线程进行处理。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
2.3 线程池中使用有限的阻塞队列和无限的阻塞队列的区别
有限阻塞队列会存在任务入队失败的情况,而无限阻塞队列则不会,他会一直往队列中添加请求,直到系统资源耗尽。
3.如何合理设置线程池队列长度
tomcat、Dubbo 等业界成熟的产品是如何设置线程队列,我们主要分析下这两个中间件
1.JDK线程池策略
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于等于corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理添加的任务。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过
handler所指定的策略来处理此任务。也就是处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。- 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
2.Tomcat线程池策略
Tomcat的线程池队列是无限长度的,但是线程池会一直创建到maximumPoolSize,然后才把请求放入等待队列中
tomcat 任务队列org.apache.tomcat.util.threads.TaskQueue其继承与LinkedBlockingQueue,覆写offer方法。
@Override
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);
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//线程个数小于MaximumPoolSize会创建新的线程。
//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);
}
3.Dubbo线程池策略
Dubbo 提供3种线程池模型即:FixedThreadPool、CachedThreadPool(客户端默认的)、LimitedThreadPool(服务端默认的),从源码可以看出,其默认的队列长度都是0,当队列长度为0 ,其使用是无缓冲的队列SynchronousQueue,当运行线程超过maximumPoolSize则拒绝请求。
4.总结
线程池的任务队列本来起缓冲作用,但是如果设置的不合理会导致线程池无法扩容至max,这样无法发挥多线程的能力,导致一些服务响应变慢。
队列长度要看具体使用场景,取决服务端处理能力以及客户端能容忍的超时时间等
建议采用tomcat的处理方式,core与max一致,先扩容到max再放队列,不过队列长度要根据使用场景设置一个上限值,如果响应时间要求较高的系统可以设置为0。