当前位置: 首页 > 科技观察

原生线程池这么强大,为什么Tomcat还要扩展线程池呢?

时间:2023-03-13 20:42:08 科技观察

前言Tomcat/Jetty是目前流行的Web容器。两者接收到请求后都会转入线程池进行处理,可以有效提高处理能力和并发度。JDK提供了完整的线程池实现,但Tomcat/Jetty都没有直接使用它。Jetty采用自研方案,内部实现QueuedThreadPool线程池组件,而Tomcat采用扩展方案,踩在JDK线程池的肩膀上,扩展JDK原生线程池。JDK原生线程池可以说是功能比较齐全,使用起来也比较简单,那为什么Tomcat/Jetty没有选择这种方案,而是自己实现呢?JDK线程池通常我们可以将执行的任务分为两类:cpu-intensiveIO-intensivetasksCPU-intensivetasks需要线程长时间执行复杂的计算。这种类型的任务需要创建的线程较少。线程过多会频繁造成上下文切换,降低任务处理速度。对于io密集型任务,由于线程并不是一直在运行,可能大部分时间都在等待IO读写数据。增加线程数可以提高并发性,处理尽可能多的任务。JDK原生线程池的工作流程如下:线程池执行流程图《详细可以查看一篇教你安全关闭线程池的文章,上图假设使用了LinkedBlockingQueue。》灵魂拷问:上面的过程是不是漏掉了?很长一段时间,我都认为线程数达到最大线程数后才放入队列。 ̄□ ̄||在上图中可以看出,只要线程池中的线程数大于核心线程数,任务就会先加入到任务队列中,只有当任务队列加入失败会创建一个新的线程。也就是说,在原生线程池队列满之前,最多只有核心线程数。这种策略显然更适合处理cpu密集型任务,但是对于io密集型任务就不是很友好了,比如数据库查询,rpc请求调用等,因为Tomcat/Jetty需要处理大量的客户端请求任务,如果使用原生线程池,一旦接受的请求数大于线程池中的核心线程数,这些请求就会被放入队列,等待核心线程处理。这样做显然降低了这些请求的整体处理速度,所以都没有使用JDK原生的线程池。解决上述问题的方法可以是像Jetty本身一样实现线程池组件,这样内部逻辑可以更适配,但是开发难度更大。另一种像Tomcat,扩展了原生的JDK线程池,实现比较简单。下面主要使用Tomcat扩展线程池,说说它的实现原理。扩展线程池首先,我们从JDK线程池的源码入手,看看如何在此基础上进行扩展。可以看出线程池的过程主要分为三个步骤。第二步根据queue#offer方法返回结果,判断是否需要创建新线程。JDK原生队列类型LinkedBlockingQueue、SynchronousQueue,两者的实现逻辑是不一样的。LinkedBlockingQueueoffer方法会以队列是否满为判断条件。如果队列已满,则返回false,如果队列未满,则将任务加入队列并返回true。SynchronousQueue是一个特殊的队列,里面不存储任何数据。如果一个线程将任务放入其中,它将被阻塞,直到其他线程将任务取出。反之,如果没有其他线程往里面放任务,从队列中取任务的方法也会被阻塞,直到其他线程往里面放任务。对于offer方法,如果其他线程正在被fetch方法阻塞,则此方法将返回true。否则,offer方法将返回false。因此,如果要实现适合io密集型任务的线程池,即优先处理新的线程处理任务,关键就在于queue#offer方法。该方法的内部逻辑可以重写。只要当前线程池数小于最大线程数,该方法就返回false,线程池创建一个新的线程进行处理。当然,上面的实现逻辑比较粗糙。下面我们从Tomcat的源码来看它的实现逻辑。Tomcat扩展线程池Tomcat扩展线程池直接继承了JDK线程池java.util.concurrent.ThreadPoolExecutor,并重写了一些方法的逻辑。另外实现了TaskQueue,直接继承了LinkedBlockingQueue,重写了offer方法。首先看看如何使用Tomcat线程池。可见,Tomcat线程池的用法与普通线程池没有太大区别。接下来我们看一下Tomcat线程池的核心方法execute的逻辑。execute方法的逻辑比较简单,任务的核心还是交给Java原生线程池处理。这里我们主要添加一个重试策略。如果本机线程池执行拒绝策略,则抛出RejectedExecutionException。这里会被捕获,然后再次尝试将任务添加到TaskQueue中,尽量执行任务。请注意此处的submittedCount变量。这是Tomcat线程池内部的一个重要参数。它是一个AtomicInteger变量,会实时统计已经提交到线程池但还未执行??的任务。也就是说,submittedCount等于线程池队列中的任务数加上线程池工作线程正在执行的任务数。TaskQueue#offer会使用这个参数来实现相应的逻辑。然后我们主要看一下TaskQueue#offer方法的逻辑。核心逻辑在第三步,如果submittedCount小于当前线程池的线程数,则返回false。上面我们提到offer方法返回false,线程池会直接新建一个线程。Dubbo2.6.X版本新增EagerThreadPool。其实现原理与Tomcat线程池类似。感兴趣的朋友可以自行阅读。一个折中的方法虽然上面的扩展方法看起来不是很难,但是自己实现起来可能成本会很高。如果不想扩展线程池来运行io密集型任务,可以使用下面的折衷方法。新的ThreadPoolExecutor(10、10、0L、TimeUnit.MILLISECONDS、新的LinkedBlockingQueue(100));但是,使用此方法会使keepAliveTime失效。线程一旦创建,就会一直存在,这是对系统资源的浪费。综上所述,JDK实现了比较完善的线程池功能,但更适合运行CPU密集型任务,而不是IO密集型任务。对于IO密集型任务,可以通过设置线程池参数间接完成。