前言java
Tomcat/Jetty 是目前比较流行的 Web 容器,二者接受请求以后都会转交给线程池处理,这样能够有效提升处理的能力与并发度。JDK 提升完整线程池实现,可是 Tomcat/Jetty 都没有直接使用。Jetty 采用自研方案,内部实现 QueuedThreadPool 线程池组件,而 Tomcat 采用扩展方案,踩在 JDK 线程池的肩膀上,扩展 JDK 原生线程池。数据库
JDK 原生线程池能够说功能比较完善,使用也比较简单,那为什么 Tomcat/Jetty 却不选择这个方案,反而本身去动手实现那?并发
JDK 线程池ide
一般咱们能够将执行的任务分为两类:高并发
cpu 密集型任务
io 密集型任务
cpu 密集型任务,须要线程长时间进行的复杂的运算,这种类型的任务须要少建立线程,过多的线程将会频繁引发上文切换,下降任务处理处理速度。线程
而 io 密集型任务,因为线程并非一直在运行,可能大部分时间在等待 IO 读取/写入数据,增长线程数量能够提升并发度,尽量多处理任务。3d
JDK 原生线程池工做流程以下:code
原生线程池这么强大,Tomcat 为什么还需扩展线程池?
上图假设使用 LinkedBlockingQueue 。blog
灵魂拷问:上述流程是否记错过?在很长一段时间内,我都认为线程数量到达最大线程数,才放入队列中。 ̄□ ̄||继承
上图中能够发现只要线程池线程数量大于核心线程数,就会先将任务加入到任务队列中,只有任务队列加入失败,才会再新建线程。也就是说原生线程池队列未满以前,最多只有核心线程数量线程。
这种策略显然比较适合处理 cpu 密集型任务,可是对于 io 密集型任务,如数据库查询,rpc 请求调用等,就不是很友好了。
因为 Tomcat/Jetty 须要处理大量客户端请求任务,若是采用原生线程池,一旦接受请求数量大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样作显然下降这些请求整体处理速度,因此二者都没采用 JDK 原生线程池。
解决上面的办法能够像 Jetty 本身实现线程池组件,这样就能够更加适配内部逻辑,不过开发难度比较大,另外一种就像 Tomcat 同样,扩展原生 JDK 线程池,实现比较简单。
下面主要以 Tomcat 扩展线程池,讲讲其实现原理。
扩展线程池
首先咱们从 JDK 线程池源码出发,查看如何这个基础上扩展。
原生线程池这么强大,Tomcat 为什么还需扩展线程池?
能够看到线程池流程主要分为三步,第二步根据 queue#offer 方法返回结果,判断是否须要新建线程。
JDK 原生队列类型 LinkedBlockingQueue , SynchronousQueue ,二者实现逻辑不尽相同。
LinkedBlockingQueue
offer 方法内部将会根据队列是否已满做为判断条件。若队列已满,返回 false ,若队列未满,则将任务加入队列中,且返回 true 。
SynchronousQueue
这个队列比较特殊,内部不会储存任何数据。如有线程将任务放入其中将会被阻塞,直到其余线程将任务取出。反之,若无其余线程将任务放入其中,该队列取任务的方法也将会被阻塞,直到其余线程将任务放入。
对于 offer 方法来讲,如有其余线程正在被取方法阻塞,该方法将会返回 true 。反之,offer 方法将会返回 false。
因此若想实现适合 io 密集型任务线程池,即优先新建线程处理任务,关键在于 queue#offer 方法。能够重写该方法内部逻辑,只要当前线程池数量小于最大线程数,该方法返回 false ,线程池新建线程处理。
固然上述实现逻辑比较糙,下面咱们就从 Tomcat 源码查看其实现逻辑。
Tomcat 扩展线程池
Tomcat 扩展线程池直接继承 JDK 线程池 java.util.concurrent.ThreadPoolExecutor ,重写部分方法的逻辑。另外还实现了 TaskQueue ,直接继承 LinkedBlockingQueue ,重写 offer 方法。
首先查看 Tomcat 线程池的使用方法。
原生线程池这么强大,Tomcat 为什么还需扩展线程池?
能够看到 Tomcat 线程池使用方法与普通的线程池差不太多。
接着咱们查看一下 Tomcat 线程池核心方法 execute 的逻辑。
原生线程池这么强大,Tomcat 为什么还需扩展线程池?
execute 方法逻辑比较简单,任务核心仍是交给 Java 原生线程池处理。这里主要增长一个重试策略,若是原生线程池执行拒绝策略的状况,抛出 RejectedExecutionException 异常。这里将会捕获,而后从新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。
这里须要注意 submittedCount 变量。这是 Tomcat 线程池内部一个重要的参数,它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但尚未执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工做线程正在执行的任务。 TaskQueue#offer 将会使用该参数实现相应的逻辑。
接着咱们主要查看 TaskQueue#offer 方法逻辑。
原生线程池这么强大,Tomcat 为什么还需扩展线程池?
核心逻辑在于第三步,这里若是 submittedCount 小于当前线程池线程数量,将会返回 false。上面咱们讲到 offer 方法返回 false ,线程池将会直接建立新线程。
Dubbo 2.6.X 版本增长 EagerThreadPool ,其实现原理与 Tomcat 线程池差很少,感兴趣的小伙伴能够自行翻阅。
折衷方法
上述扩展方法虽然看起不是很难,可是本身实现代价可能就比较大。若不想扩展线程池运行 io 密集型任务,能够采用下面这种折衷方法。
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100));
不过使用这种方式将会使 keepAliveTime 失效,线程一旦被建立,将会一直存在,比较浪费系统资源。
总结
JDK 实现线程池功能比较完善,可是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务。对于 IO 密集型任务能够间接经过设置线程池参数方式作到。