• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

深入理解 Java 线程池

技术杂谈 勤劳的小蚂蚁 3个月前 (01-27) 76次浏览 已收录 0个评论 扫描二维码

今天写代码的时候,用到了线程池,但是由于资源有限,有可能有的任务可能会被丢弃,由于是回调第三方接口,所以我想把丢弃掉的任务信息记录到日志里,方便后续问题定位。这就需要自定义任务拒绝策略。回想到面试遇到的一些线程池问题,决定整理一下相关信息,所以这篇文章就诞生了。

一、如何构建线程池?

我相信多数用过线程池的Java程序员都用过Executors来创建线程池,该类提供了几个静态方法,可以快速创建线程池。


如上图所示,可以创建四种类型的线程池

  • 固定线程数量的线程池。
  • 根据需要创建线程的线程池。
  • 执行定时任务的线程池。
  • 单个线程的线程池。

多数情况下,这几种类型的线程池就能满足我们的需要。但是实际上还有一个创建线程池的方法那就是手动构造线程池(ThreadPoolExecutor)

二、ThreadPoolExecutor

如果你去看一下前面提到的Executors的几个静态方法的实现,你会发现他们其实就用到了ThreadPoolExecutor,只是根据不同的场景传入了不同的参数。完整的ThreadPoolExecutor总共有七个参数 如图


  • corePoolSize。核心线程数
  • maximumPoolSize。最大线程数
  • keepAliveTime。线程存活时间
  • unit。 存活时间的单位
  • workQueue。 工作队列
  • threadFactory。构造线程池中线程的工厂
  • handler。任务不能被处理时的拒绝策略

三、工作原理(流程)

上面列出的 几个参数,我虽然都给了中文解释,但是如果不结合原理来描述一下他们的具体作用,有些参数我感觉还是不好理解。所以这里就把线程池工作原理和参数一起讲。

  • 提交任务的时候,判断当前线程池中的存活线程数量是否小于corePoolSize
  • 如果小于corePoolSize,则不管是否有线程处于空闲状态,都会新建一个线程。
  • 如果线程数量已经达到corePoolSize,则将任务扔进队列workQueue
  • 随着任务越来越多,队列可能已经满了,则需要看当前线程是否已经达到了maximumPoolSize,如果没有达到,则创建新的线程,并用它执行该任务。
  • 最坏情况,任务实在太多了,队列已经满了,而线程数量已经达到maximumPoolSize,还有新的任务来,说白了,就是已经满负荷了,任然还有任务需要执行,这个时候就会handler来处理该任务了。

整个工作原理就这样了,标红部分尤其重要,面试稍微深入一点,肯定就会问到这个,一定记住,是先将任务扔进队列,队列满了之后才会继续考虑创建线程。至于为什么要这样设计,可以想一想,想不通可以给我发消息。整个原理说下来,还有3个参数没有提到,这里再说明一下他们的作用

  • keepAliveTime。如我们所知,使用线程池的目的就是为了减少线程的创建,因为创建线程本身是比较耗资源的。由于线程本身需要占用资源,有一种情况就是,某个时候线程数量比较多,但是任务没有多少,就会出现有的线程没有活干,所以我们就可以考虑释放掉其资源,但是呢,我们又无法预知未来的任务量,所以我们就准许其空闲一段时间,如果过了这段时间都还是空闲的,那么就会释放掉其资源(就像在公司上班,可能有段时间没活干,老板可能并不会让你走,要是长时间没活干,老板可能就为了节约成本,要裁员了),这个参数就是用指定这段空闲时间的。默认情况下是有超过corePoolSize个线程时,就会用到该值, 但是也可以指定corePoolSize数量之内的线程空闲时是否释放资源(allowCoreThreadTimeout)。(就类似默认情况下,公司肯定只会裁掉非核心员工,但是实在混不走的时候,核心员工可能也会被干掉)

  • unit 这个参数很好理解,就是单位,就是前面keepAliveTime这个我们准许空闲的时间的单位

  • factory .其类型为ThreadFactory,顾名思义,就是一个创建Thread的Factory. 该接口只有一个方法,产生一个Thread。通常情况下,我们都只需要使用默认的factory就可以了,但是为了定位问题方便,我们可以为线程池创建的线程设置一个名字,这样看日志的时候就比较方便了。

四、RejectExecutionHandler

这个拒绝策略有必要拿出来单独说一下,我今天就是实现了该接口,从而满足了业务需要。

为什么我需要自己实现该接口,而采用Executors静态方法时,并没有让传入该参数呢?实际上Jdk本身提供了四种策略,分别是

  • AbortPolicy。会抛出异常
  • CallerRunnerPolicy。在调用execute的方法中执行被拒绝的任务
  • DiscardOldestPolicy。丢掉队列中最老的任务,然后重试
  • DiscardPolicy。直接丢掉该任务

这四种策略是ThredPoolExecutor的内部类,实现都比较简单,有兴趣的可以看一下。我今天的实现方式也很简单,实际上就是在discardPolicy的基础上增加日志记录。

五、其他

前面说了其工作原理,但是看了一下源代码,其实和描述的原理并不完全一致。主要在处理队列大小的时候,主要是对正在运行的线程数量还个判断,不能超过指定的值,当然这个值比较大,我们一般不会达到这个值,至于具体原因我也么去继续深入研究。

其次就是参数设置,可能需要具体业务场景,任务数量,任务执行速度来调整,并没有一个固定的值。只是记得一定要设置队列大小,不然就使用了一个无界队列,可能就是会内存爆掉。

实际使用场景下,还有一些可以优化的地方,比如对不同类型的任务创建不同的线程池, 比如有的线程比较耗时,有的很快,如果放在同一个线程池里面执行,可能导致队列很快就满了,本来该很快执行完的任务却一直得不到执行。

另外还有ThreadPoolExecutor还提供了一些hook方法,如有需要可以使用

  • beforeExecute()  任务执行之前调用
  • afterExecute() 任务执行之后调用

虽然有点标题党,说是深入理解,其实也并不是特别深入,但是基本上这篇文章的内容掌握过后,个人觉得起码90%以上的问题都能对答如流了。还有10%在哪里? 可以去看一下newCachedThreadPool的实现,他使用的队列不一样。还有就是想一想,如果实现线程存活的功能,让自己实现,怎么来做。另外可能就是真的需要去看一下源码,把具体的worker创建运行过程都搞透彻了。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:深入理解 Java 线程池
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00