【线程池】线程池拒绝策略还有这个大坑(二)

目录

踩坑代码

后果展示

原因

小结


概要

上文我们聊了聊阻塞队列,有需要的小伙伴可以去瞅瞅【线程池】换个姿势来看线程池中不一样的阻塞队列(一)_走了一些弯路的博客-CSDN博客

这波我们一起来研究下线程池的拒绝策略。

你肯定要说了,拒绝策略不就这四个吗,没有技术含量,有啥好聊的

【线程池】线程池拒绝策略还有这个大坑(二)_第1张图片

一般要不然是默认的拒绝策略,要不然就是不重要的流程发个告警,连异常都懒得抛,这里得画个重点

欸,说到连异常都懒得抛,那我就不得不来说一说曾经在拒绝策略上踩的坑了。


踩坑代码

如下代码展示

【线程池】线程池拒绝策略还有这个大坑(二)_第2张图片

 堆内存大小配置为10m

【线程池】线程池拒绝策略还有这个大坑(二)_第3张图片

 代码前期正常输出如下,实现的拒绝策略正常打印了语句,线程池在一个一个的消费任务。

【线程池】线程池拒绝策略还有这个大坑(二)_第4张图片

后果展示

      然鹅,在持续输出半个多小时之后,程序员们人人色变,但面试又侃侃而谈的OOM理所应当的出现了,在我的世界里,带给我惊喜

【线程池】线程池拒绝策略还有这个大坑(二)_第5张图片

        没办法,按照中国宝宝的体制,长达半个多小时,搁谁也受不了,更何况堆内存只设置了10m。

        那么是谁有问题呢,我们该怎么揪出造成OOM的内鬼呢,毕竟出了这么大的事情总得有人来背锅吧。

        虽说本文的主体是在聊拒绝策略的问题,但总不能不查个清楚,就直接定罪逮捕刚刚写的拒绝策略吧,总得有个狡辩的过程~

        华生,走,去看看犯罪现场吧。


排查过程

      重启刚刚的那段代码, 打开JDK自带的jvisualvm

【线程池】线程池拒绝策略还有这个大坑(二)_第6张图片

       

          连接上本地正在运行的代码后,观察十多分钟之后,可以看到右上方堆内存的火焰图以及下方老年代的大小。

         惊不惊喜,熟不熟悉,看看这节节攀升的堆内存,看看这努力的垃圾回收器,GC的这么频繁,但是堆内存仍然在不断增长,在到达大约8M之后,停滞不对,OOM

【线程池】线程池拒绝策略还有这个大坑(二)_第7张图片

【线程池】线程池拒绝策略还有这个大坑(二)_第8张图片

        


        

         我们dump下当前的堆内存,很快就可以找到占比较大的可疑内存对象 ---》FutureTask
【线程池】线程池拒绝策略还有这个大坑(二)_第9张图片

        

        再看下应用也产生了很多的线程。基本上都阻塞在,下面的这行代码

String s = result.get();

【线程池】线程池拒绝策略还有这个大坑(二)_第10张图片

【线程池】线程池拒绝策略还有这个大坑(二)_第11张图片

        让我们来一起狠狠的撕开

FutureTask.get()

        方法的神秘面纱,为什么会阻塞在这里。

原因

   首先我们很容易可以确定,以上大量线程阻塞的地方是执行拒绝策略的线程。

   让我们直接定位到FutureTask线程阻塞的代码 #get()方法,很明显这里的state不正常,导致了线程阻塞在这里。 【线程池】线程池拒绝策略还有这个大坑(二)_第12张图片        为什么state会不正常呢,继续跟进去看#awaitDone()方法,线程在这里阻塞住了,等待唤醒。

【线程池】线程池拒绝策略还有这个大坑(二)_第13张图片

         我们在这里插入一个知识点,可以下篇文章来聊聊线程池的异常是怎么处理的,这里就不展开说了,抛个结论先。

        在使用线程池时,如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。

        线程的状态流转只有以下几种,也不会整出别的花活。

  • * NEW -> COMPLETING -> NORMAL
    * NEW -> COMPLETING -> EXCEPTIONAL
    * NEW -> CANCELLED
    * NEW -> INTERRUPTING -> INTERRUPTED

【线程池】线程池拒绝策略还有这个大坑(二)_第14张图片

        按照正常理解,这个地方的执行了拒绝的策略的线程状态应该是EXCEPTIONAL

        但实际上,以上线程阻塞的地方,state <  COMPLETING,只能是NEW

        那么问题变成了为什么执行了测试代码中拒绝策略的线程状态会是NEW

        之后的状态EXCEPTIONAL去哪儿了呢

        朋友们,来回顾下我们刚刚插入的知识点,FutureTask的run方法呀

【线程池】线程池拒绝策略还有这个大坑(二)_第15张图片

【线程池】线程池拒绝策略还有这个大坑(二)_第16张图片

 

         

那么执行拒绝策略的线程,还会继续run吗?当然不会啦。

        所以Future.get()就像苦苦等一个不回头的人,除了浪费时间、浪费资源,没有任何意义,还会造成严重的后果(OOM)。


解决方案

        1.对于线程池的拒绝策略不要静默处理,也就是我demo代码中,只打印了一行日志,什么也不做,哪怕抛个业务异常呢

        2.能用execute()提交任务就使用execute(),除非线程池有返回值才用submit()。

        另外JDK的issue中也有类似的讨论,关于线程池的静默处理的拒绝策略是不是个BUG,有兴趣的同学也可以去看下

[JDK-8286463] DiscardPolicy may block invokeAll forever - Java Bug System


小结

        无论是线程池自带的静默处理的拒绝策略DiscardPolicy,还是我们花里胡哨实现了个类似静默处理的拒绝策略,既不抛出异常也不放回队列的,使用Future.get()会是线程阻塞,在某些情况下,会导致内存溢出。

你可能感兴趣的:(线程池,java,拒绝策略,并发编程,多线程)