内容简介:很久没写关于Jetty的博客了,这次又为大家带来了干货,Jetty中的重中之重,线程池,希望大家能喜欢~Jetty里面存在大量的基础组件,其中最核心之一就是QueuedThreadPool(后面都简称qtp),它是一个线程池。Jetty默认会使用qtp作为任务任务执行容器,包括连连接的建立、连接处理、IO读写事件、业务处理等等
很久没写关于Jetty的博客了,这次又为大家带来了干货,Jetty中的重中之重,线程池,希望大家能喜欢~
二、概念
Jetty里面存在大量的基础组件,其中最核心之一就是QueuedThreadPool(后面都简称qtp),它是一个线程池。Jetty默认会使用qtp作为任务任务执行容器,包括连连接的建立、连接处理、IO读写事件、业务处理等等
三、继承体系
QueuedThreadPool实现了LifeCycle,也就是具有生命周期的概念,同时它还是一个有界线程池(SizedThreadPool),实现了JDK的Executor,标识自己也是一个符合JDK规范的线程池
四、总体架构
说到线程池就包括几部分,启动、停止、运行,其中运行的过程包括扩容、缩容、对中断和异常的处理,qtp主要包括如上图的几部分逻辑,其中运行阶段是最核心的部分
五、源码剖析
1. 创建
构造函数一层一层调到最后一个方法,只是上面会默认设置一些参数
最后一个方法里面可以看到,设置最小、最大线程数,设置线程存活超时,线程停止超时,如果没有指定任务队列,自动创建一个BlockingArrayQueue(注意不是ArrayBlockingQueue,Jetty这里声明的这个Queue是自己实现的一个可以自动扩容、缩容的阻塞队列),最后设置线程组
2. 启动
启动看起来很简单,首先调LifeCycle的doStart,然后把当前启动线程数设置为0,表示没有启动任何一个线程,然后调startThreads,传入了最小线程数(通常就是8),接下来我们再来看startThreads方法,如下图
首先明显让我们看到的就是这个方法无锁,用了CAS,大while循环保证当前线程最终能获取到线程创建的权限
while里面可以看到先拿乐观锁,拿到了就创建线程并启动,同时要创建的线程数量减1,直到创建完所有要求的线程数量
这里我们需要关注的就是newThread方法传入了一个_runnable,这个是共享的执行逻辑,所有的线程池中的线程都执行这个逻辑
(后面会详细解释),同时可以看到线程的名称是_name和线程id的组合,_name其实就是”qtp” + 当前线程池的hashCode
3. 运行
3.1. 生产任务
生产任务就是外界调用execute方法,传入一个Runnable任务进来
这里可看到,如果当前队列满了,就抛拒绝执行异常,否则如果没有线程,就会启动一个
3.2. 消费任务
前面提到过,在线程池中的线程全部会执行同一个runnable逻辑,如下图
这个就是qtp的核心逻辑了,这里会获取任务,执行任务,如果当前线程池处于空闲状态,就会尝试缩容,如果线程池线程不够用(线程数还没达到线程池最大大小)则会扩容线程
从上面可以看到整个逻辑分为2大部分:忙碌循环(上图中的Job loop)、空闲循环(上图中的Idle loop)
忙碌循环:线程处于不断消费任务,并执行任务的阶段
空闲循环:没有任务可以执行,空转等待新的任务
4. 扩容
扩容分为2初,第一处是当前线程刚被启动执行的时候,如下图
这种情况扩容一般是当前任务数量很大,线程被占完了,直接扩容1个,保证任务被执行掉
第二部分扩容就是在线程进入空闲等待循环后,发现来了新的任务,但是这个时候刚好又没有空闲线程,于是就扩容一个,来保证任务执行
从这里可以看到,如果当前线程池中线程都处于忙碌状态,qtp不会执行扩容,而是在不断处理任务,只有在进入空闲循环后,发现线程数不够才扩容
5. 缩容
缩容主要发生在空闲循环中,没有任务可以执行,qtp会循环等待新的任务,如果当前线程的空闲超时<=0直接就阻塞等,否则就按照超时时间等待,这里也是无锁,利用CAS来修改上一次缩容时间和启动的线程总数,如果发现当前达到了线程空闲超时,就会退出整个Runnbale顶层loop循环,即表示退出当前线程,也就是缩容了,不过缩容还得修改一些数据,如下图
退出循环后,可以看到会移除当前线程的引用。
另外值得注意的一点是,如果不是缩容,但是又在运行的状态,说明线程出现了异常退出,如果线程数小于最大线程数,这里还会再扩一个线程,也就是说线程异常退出,qtp仍然能再创建一个新的到线程池,保证线程数的稳定
六、实战分析
1. 启动jetty.project下面的ExampleServer
2. jstack $pid
可以看到这些线程名称都以qtp开头,其实就是qtp+hashCode+编号,这些线程大部分处于超时等待状态,而少部分处于运行状态。
其中18、19号线程处于TIMED_WAITING状态,触发的方法是idleJobPoll(),上面的分析我们看到就是当前线程池的任务队列(jobs)没有,因此进入了空闲循环等待新的任务到来。
而17号线程处于RUNNABLE状态,是由runJob()触发,可以看到调用的是Acceptor组件的run方法,即等待新的连接接入
剩下的16号线程也处于RUNNABLE,这种也必然是runJob()触发,可以看到这个在做kqueue的IO事件等待
七、QueuedThreadPool与JDK线程池(ThreadPoolExecutor,下称tpe)的异同
看完了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的线程池,它的代码清晰易懂,感兴趣的读者下来可以深入研究~
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Flink 核心组件原理多图剖析
- 深入剖析Vue源码 - 组件基础
- 深入剖析Vue源码 - 组件进阶
- 剖析 Android 架构组件之 ViewModel
- SpringCloud组件 & 源码剖析:Eureka服务注册方式流程全面分析
- 蚂蚁金服分布式链路跟踪组件 SOFATracer 总览 | 剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Mechanics of Web Handling
David R. Roisum
This unique book covers many aspects of web handling for manufacturing, converting, and printing. The book is applicable to any web including paper, film, foil, nonwovens, and textiles. The Mech......一起来看看 《The Mechanics of Web Handling》 这本书的介绍吧!