前言
本文中无特别说明的话,线程池指的是
java.util.concurrent #ThreadPoolExecutor
本文只探讨线程池中阻塞队列相关,暂时不对线程池的其他方面进行说明,如果对线程池感兴趣的话,接下来几天我会多分享些和线程池相关的知识,和大家探讨下,比如:
- 核心线程数及最大线程数如何根据业务场景进行合适的配置
- 线程池中的异常是如何抛出的
- 拒绝策略应该如何选择,有哪些坑
- 如何解决使用线程池异步操作时的上下文传递
阻塞队列在线程池中的常见使用
ThreadPoolExecutor为了方便使用,提供了多个用来初始化的构造函数,我们来看看其中的一个常用的构造函数,下面第五个参数BlocklingQueue类型的workQueue就是线程池中的阻塞队列。
提到线程池的时候,我们往往会想到池化技术、线程复用、线程管理等,而我理解其核心机制包括两个方面
- 线程的管理
核心线程数、最大线程数的控制
- 任务的管理
任务什么时候需要存储、存储在哪里、怎么消费等
阻塞队列是任务管理的核心实现,在多样的业务需求下,不同的阻塞队列,可以定制化的控制任务处理以及线程池中活跃线程的个数等,从而应对不同的业务场景需要。
以下三个方面,阻塞队列不同的使用策略
-
直接使用线程处理
不使用队列对提交的任务进行缓存,
例如使用SynchronousQueue 见名知意,此队列只提供了同步的功能
SynchronousQueue是一个没有数据缓冲的BlockingQueue。 一个线程的插入必须等待另一个线程的删除操作才能完成,反之亦然。 SynchronousQueue适合传递性设计(handoff designs),即一个线程中运行的对象,需要将某些信息、任务或者事件等传递给另一个线程中运行的对象的场景。 SynchronousQueue支持公平和非公平模式。 不能在同步队列上进行peek,因为仅在试图要移除元素时,该元素才存在。 不能迭代队列,因为其中没有元素可用于迭代。 Executors.newCachedThreadPool()中就使用了SynchronousQueue队列
-
无界队列
最经典的LinkedBlockingQueue,基于内存可以无限大的队列。
也是面试会问到阿里巴巴为什么不推荐使用Executors创建线程池,原因像如下线程池使用的阻塞队列是LinkedBlockingQueue,在高并发请求的情况下,无界队列会造成内存溢出(OOM)。
Executors.newFixedThreadPool(5);
Executors.newSingleThreadExecutor();
-
有界队列
通过初始化阻塞队列的大小来控制可提交至线程池任务的个数,可根据业务场景的估算,设置队列的大小,如下图所示使用
ArrayBlockingQueue
你知道tomcat是怎么使用线程池的吗
tomcat作为最常用的web服务器之一,其意义相对于HTTP请求来说,由于西方不能失去耶路撒冷,葫芦娃不能失去爷爷,野比大雄不能失去叮当猫,我们非常有必要来了解下,当tomcat接收到请求时,内部线程池是怎样使用的,阻塞队列起到了什么样的作用。
当然了,你根本不知道tomcat有多努力,呸,你这个渣男。
我们都知道对于JUC包下的ThreadPoolExecutor的任务提交时,任务执行机制流程是这样式的,贴一下来自美团技术团队的那篇文章(墙裂推荐大家去好好看一下,把玩其中的奥妙)的经典插图。
关键点是在当阻塞队列已满时,线程池会继续添加工作线程直至到达最大线程数后,再次提交任务,会执行任务拒绝策略(这里拒绝策略的选择是有个大坑的,后续我再发文细说),
当然了这里有个关键的前提,阻塞队列必须是个有界的队列。
但是呢,请注意。
tomcat在上述流程的处理采取了不一样的方式
tomcat线程池的实现位于tomcat-embed-core-9.0.37.jar下,
org.apache.tomcat.util.threads # ThreadPoolExecutor
名称和JUC包下的线程池名称一模一样,
去github找到tomcat的ThreadPoolExecutor源码和JUC包下的ThreadPoolExecutor对比,几乎一模一样甚至注释也一样。
只不过tomcat针对自身的使用业务场景做了一点改动,从而更贴合自身的使用
以下是tomcat线程池的入口处
主要改动:tomcat线程池在线程大小到达核心线程数之后,并不直接入队列,而是先添加工作线程数之后再继续入队列,而这个队列是一个无界队列。
tomcat这样做的好处是什么呢?
先明确一下,tomcat是用来接收web的Http请求的,并发处理的请求数是要进行限制的,但是呢又不能限制业务请求进入系统,而上述流程确实满足了上述要求
- 使用最大线程数来处理可能到来的瞬时流量,当请求变少时,闲置的线程也可以被回收
- 使用无界队列来接收最大线程数也无法处理的任务,起到一个削峰添谷的作用。
那么tomcat是如何实现的呢
没错儿,就是使用自定义的阻塞队列TaskQueue来实现的。
关键点在于当活跃线程数达到核心线程数时,继续提交任务正常是会进队列,tomcat主体流程也是进队列,但是它搞了个骚操作换了个玩法,不得不让人竖起大拇哥,真是神奇他妈给神奇开门--神奇到家了。
如下所示,TaskQueue是继承自LinkedBlockingQueue,但是它添加元素的offer()方法
中,当核心线程数小于最大线程数时,会返回false。
offer()方法返回false之后,会继续创建线程addWorker()执行提交的任务,直至达到最大线程数之后,才会继续提交任务至无界队列中。
tomcat中结合其应用场景,自定义阻塞队列达到了其预期的使用效果。
Dubbo中阻塞队列的使用姿势绝对是你未见过的船新版本
不仅渣渣辉代言的传奇是船新版本,Dubbo中的阻塞队列我第一次见到时也是被惊艳到了,这个全新版本才是小刀剌(la)屁股---开了眼了。
在GitHub冲浪时,在dubbo的issue里发现了这么个好东西,也帮大伙儿开开眼。
这个issue要给dubbo中的线程池添加一个阻塞队列,MemorySafeLinkedBlockingQueue为了防止和下文中要讲解的另一个相似的队列混淆,我们简称这个队列为MMS 。[ISSUE #10020] add MemorySafeLinkedBlockingQueue by dragon-zhang · Pull Request #10021 · apache/dubbo · GitHub
看这个标题你是不是虎躯一震,看看这几个关键词,多么有冲击力
MemorySafe --内存安全
solve OOM problem --解决内存溢出问题
easier than MemoryLimitedLinkedBlockingQueue --比限制内存的阻塞队列更容易
在看这个issue提出的MemorySafeLinkedBlockingQueue之前,我们先来深入了解下上一个版本的MemoryLimitedLinkedBlockingQueue蕴含着怎么样的奥妙。
MemoryLimitedLinkedBlockingQueue ---MML
以下我们简称MemoryLimitedLinkedBlockingQueue为 MML。
现在在dubbo的issue中搜索一下这个阻塞队列是什么时候提交的,是解决了什么问题。找到了以下三个issue。
可以看到MML目的是为了解决线程池中无界队列可能导致的OOM问题
上图中ThreadPool就是dubbo中线程池自定义实现类,可以看到,提交者希望使用MML替换了原有的阻塞队列。
来看下内部的实现
核心逻辑是通过记录已使用内存,可配置的限制内存的大小,借助Instrumentation接口来获取当前要提交任务对象的大小,这三项来控制阻塞队列的占用内存大小,从而避免OOM的出现。
熟悉像javaagent、Arthas的童鞋,应该会对Instrumentation接口很熟悉,可以监控JVM的底层信息,进行插桩式开发
MemorySafeLinkedBlockingQueue ---MMS
接下来我们来看下开始的时候提出的MMS是怎么防止OOM的
其实MMS和上文聊的MML是反其道而行之,上文是控制最大使用内存,而MMS是控制JVM剩余内存即未使用内存的大小。
MMS通过一个参数maxFreeMemory就实现了MML较为复杂的计算,更关键的是MMS并不需要关注添加至队列的每个任务对象的大小,也不需要累加已使用的内存,只需要关注剩余的内存就好,降低了复杂度,实现起来也非常优雅。
核心类的作用是内部另起了一个定时线程来准实时的更新剩余内存的大小
总体而言,MMS的实现更为简单轻量级,且用起来更方便。
代码的世界总是那么的出人意料又合乎情理,很多时候不明白的代码原理,兜兜转转一圈之后,见识过更多精妙的设计,蓦然回首才发现原来真理已在身边久矣。
正如本文介绍的线程池阻塞队列,在没见识到这些开源项目之前,我给它的用法贴了平平无奇的标签,我也没想到他们可以玩的这么花。
一路走来,确实也见过不少风景,来让我描绘出来吧。
【下集预告】---线程池中拒绝策略使用不当的巨坑。
都看到这里了,不如点个赞再走吧~~~