很久没写关于Jetty的博客了,这次又为大家带来了干货,Jetty中的重中之重,线程池,希望大家能喜欢~
Jetty里面存在大量的基础组件,其中最核心之一就是QueuedThreadPool(后面都简称qtp),它是一个线程池。Jetty默认会使用qtp作为任务任务执行容器,包括连连接的建立、连接处理、IO读写事件、业务处理等等
QueuedThreadPool实现了LifeCycle,也就是具有生命周期的概念,同时它还是一个有界线程池(SizedThreadPool),实现了JDK的Executor,标识自己也是一个符合JDK规范的线程池
说到线程池就包括几部分,启动、停止、运行,其中运行的过程包括扩容、缩容、对中断和异常的处理,qtp主要包括如上图的几部分逻辑,其中运行阶段是最核心的部分
构造函数一层一层调到最后一个方法,只是上面会默认设置一些参数
最后一个方法里面可以看到,设置最小、最大线程数,设置线程存活超时,线程停止超时,如果没有指定任务队列,自动创建一个BlockingArrayQueue(注意不是ArrayBlockingQueue,Jetty这里声明的这个Queue是自己实现的一个可以自动扩容、缩容的阻塞队列),最后设置线程组
启动看起来很简单,首先调LifeCycle的doStart,然后把当前启动线程数设置为0,表示没有启动任何一个线程,然后调startThreads,传入了最小线程数(通常就是8),接下来我们再来看startThreads方法,如下图
首先明显让我们看到的就是这个方法无锁,用了CAS,大while循环保证当前线程最终能获取到线程创建的权限
while里面可以看到先拿乐观锁,拿到了就创建线程并启动,同时要创建的线程数量减1,直到创建完所有要求的线程数量
这里我们需要关注的就是newThread方法传入了一个_runnable,这个是共享的执行逻辑,所有的线程池中的线程都执行这个逻辑
(后面会详细解释),同时可以看到线程的名称是_name和线程id的组合,_name其实就是”qtp” + 当前线程池的hashCode
生产任务就是外界调用execute方法,传入一个Runnable任务进来
这里可看到,如果当前队列满了,就抛拒绝执行异常,否则如果没有线程,就会启动一个
前面提到过,在线程池中的线程全部会执行同一个runnable逻辑,如下图
这个就是qtp的核心逻辑了,这里会获取任务,执行任务,如果当前线程池处于空闲状态,就会尝试缩容,如果线程池线程不够用(线程数还没达到线程池最大大小)则会扩容线程
从上面可以看到整个逻辑分为2大部分:忙碌循环(上图中的Job loop)、空闲循环(上图中的Idle loop)
忙碌循环:线程处于不断消费任务,并执行任务的阶段
空闲循环:没有任务可以执行,空转等待新的任务
扩容分为2初,第一处是当前线程刚被启动执行的时候,如下图
这种情况扩容一般是当前任务数量很大,线程被占完了,直接扩容1个,保证任务被执行掉
第二部分扩容就是在线程进入空闲等待循环后,发现来了新的任务,但是这个时候刚好又没有空闲线程,于是就扩容一个,来保证任务执行
从这里可以看到,如果当前线程池中线程都处于忙碌状态,qtp不会执行扩容,而是在不断处理任务,只有在进入空闲循环后,发现线程数不够才扩容
缩容主要发生在空闲循环中,没有任务可以执行,qtp会循环等待新的任务,如果当前线程的空闲超时<=0直接就阻塞等,否则就按照超时时间等待,这里也是无锁,利用CAS来修改上一次缩容时间和启动的线程总数,如果发现当前达到了线程空闲超时,就会退出整个Runnbale顶层loop循环,即表示退出当前线程,也就是缩容了,不过缩容还得修改一些数据,如下图
退出循环后,可以看到会移除当前线程的引用。
另外值得注意的一点是,如果不是缩容,但是又在运行的状态,说明线程出现了异常退出,如果线程数小于最大线程数,这里还会再扩一个线程,也就是说线程异常退出,qtp仍然能再创建一个新的到线程池,保证线程数的稳定
可以看到这些线程名称都以qtp开头,其实就是qtp+hashCode+编号,这些线程大部分处于超时等待状态,而少部分处于运行状态。
其中18、19号线程处于TIMED_WAITING状态,触发的方法是idleJobPoll(),上面的分析我们看到就是当前线程池的任务队列(jobs)没有,因此进入了空闲循环等待新的任务到来。
而17号线程处于RUNNABLE状态,是由runJob()触发,可以看到调用的是Acceptor组件的run方法,即等待新的连接接入
剩下的16号线程也处于RUNNABLE,这种也必然是runJob()触发,可以看到这个在做kqueue的IO事件等待
看完了Jetty的qtp实现,我们发现qtp实现还是比较简单,我想很多读者会问,qtp和JDK的ThreadPoolExecutor相比两者的优缺点是什么呢?
我们可以从两者服务的场景来分析,Jetty本身就是一个Web容器,自然就是服务于Web这块领域,Web这块领域更多的场景是IO密集型,例如IO事件的处理,业务处理,所以qtp更偏向于这块的设计,从扩容上我们可以看到qtp比较保守,任务量很大,也不轻易扩,而JDK的tpe就可能会扩,对于IO密集型扩多了还是比较浪费的
而ThreadPoolExecutor是面向整个Java体系设计的线程池,它要保证多业务场景,例如IO密集、CPU密集这些业务,都要能兼容掉,因此它的设计会更加复杂,因此参数配置都会更多,实现上面也会更灵活一些
下面我们通过一个表格来详细分析
指标/组件名称 | QueuedThreadPool(qtp) | ThreadPoolExecutor(tpe) |
---|---|---|
运行 | 共享Runnable任务执行逻辑 | 共享runWorker任务执行逻辑, 支持很多扩展点(如beforeExecute、afterExecute) |
扩容 | 任务多,线程不够用才扩容;或进入空闲状态突然来了任务才扩容(当然是线程不够用的情况) | 没达到核心数扩容;或达到了核心数,但任务队列满了且线程数没超过最大值,扩容 |
缩容 | 无任务执行,线程处于空闲状态并且达到了超时时间 | 无任务执行时,总线程数大于核心线程数(或允许核心线程缩容) |
任务溢出 | 直接拒绝 | 根据拒绝策略拒绝任务 |
灵活性 | 一般 | 较好 |
实现难度 | 简单 | 复杂 |
总结 | qtp默认启动会创建最小线程数的线程,在新的任务进来时,不一定执行扩容(如果当前任务很多),而是在线程空闲后来了任务才扩容,保证线程处于一个低水位的情况 | tpe每扔进一个任务就会判断是否需要扩容,例如核心线程数不够,或者任务队列满了但没达到最大线程数就会扩容 |
总得来说JDK的tpe还是更灵活一点,qtp更多是针对Web的场景设计的,另外qtp自己还做了queue优化,BlockingArrayQueue循环数组阻塞自增长队列,听着就很6啊~
Jetty的qtp线程池是非常核心的组件,它为上层的业务提供了强力的支撑。相比于JDK的线程池,它的代码清晰易懂,感兴趣的读者下来可以深入研究~