目录
踩坑代码
后果展示
原因
小结
概要
上文我们聊了聊阻塞队列,有需要的小伙伴可以去瞅瞅
【线程池】换个姿势来看线程池中不一样的阻塞队列(一)_走了一些弯路的博客-CSDN博客
这波我们一起来研究下线程池的拒绝策略。
你肯定要说了,拒绝策略不就这四个吗,没有技术含量,有啥好聊的
一般要不然是默认的拒绝策略,要不然就是不重要的流程发个告警,连异常都懒得抛,这里得画个重点。
欸,说到连异常都懒得抛,那我就不得不来说一说曾经在拒绝策略上踩的坑了。
踩坑代码
如下代码展示
堆内存大小配置为10m
代码前期正常输出如下,实现的拒绝策略正常打印了语句,线程池在一个一个的消费任务。
后果展示
然鹅,在持续输出半个多小时之后,程序员们人人色变,但面试又侃侃而谈的OOM理所应当的出现了,在我的世界里,带给我惊喜。
没办法,按照中国宝宝的体制,长达半个多小时,搁谁也受不了,更何况堆内存只设置了10m。
那么是谁有问题呢,我们该怎么揪出造成OOM的内鬼呢,毕竟出了这么大的事情总得有人来背锅吧。
虽说本文的主体是在聊拒绝策略的问题,但总不能不查个清楚,就直接定罪逮捕刚刚写的拒绝策略吧,总得有个狡辩的过程~
华生,走,去看看犯罪现场吧。
排查过程
重启刚刚的那段代码, 打开JDK自带的jvisualvm
连接上本地正在运行的代码后,观察十多分钟之后,可以看到右上方堆内存的火焰图以及下方老年代的大小。
惊不惊喜,熟不熟悉,看看这节节攀升的堆内存,看看这努力的垃圾回收器,GC的这么频繁,但是堆内存仍然在不断增长,在到达大约8M之后,停滞不对,OOM。
我们dump下当前的堆内存,很快就可以找到占比较大的可疑内存对象 ---》FutureTask。
再看下应用也产生了很多的线程。基本上都阻塞在,下面的这行代码
String s = result.get();
让我们来一起狠狠的撕开
FutureTask.get()
方法的神秘面纱,为什么会阻塞在这里。
原因
首先我们很容易可以确定,以上大量线程阻塞的地方是执行拒绝策略的线程。
让我们直接定位到FutureTask线程阻塞的代码 #get()方法,很明显这里的state不正常,导致了线程阻塞在这里。
为什么state会不正常呢,继续跟进去看#awaitDone()方法,
线程在这里阻塞住了,等待唤醒。
我们在这里插入一个知识点,可以下篇文章来聊聊线程池的异常是怎么处理的,这里就不展开说了,抛个结论先。
在使用线程池时,如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。
线程的状态流转只有以下几种,也不会整出别的花活。
-
* NEW -> COMPLETING -> NORMAL * NEW -> COMPLETING -> EXCEPTIONAL * NEW -> CANCELLED * NEW -> INTERRUPTING -> INTERRUPTED
按照正常理解,这个地方的执行了拒绝的策略的线程状态应该是EXCEPTIONAL
但实际上,以上线程阻塞的地方,state < COMPLETING,只能是NEW
那么问题变成了为什么执行了测试代码中拒绝策略的线程状态会是NEW呢
之后的状态EXCEPTIONAL去哪儿了呢
朋友们,来回顾下我们刚刚插入的知识点,FutureTask的run方法呀
那么执行拒绝策略的线程,还会继续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()会是线程阻塞,在某些情况下,会导致内存溢出。