
源码分析-使用newFixedThreadPool线程池导致的内存飙升问题
前言
使用无界队列的线程池会导致内存飙升吗?面试官经常会问这个问题,本文将基于源码,去分析newFixedThreadPool线程池导致的内存飙升问题,希望能加深大家的理解。
内存飙升问题复现
实例代码
配置Jvm参数
IDE指定JVM参数:-Xmx8m -Xms8m :
执行结果
run以上代码,会抛出OOM:
JVM OOM问题一般是创建太多对象,同时GC 垃圾来不及回收导致的,那么什么原因导致线程池的OOM呢?带着发现新大陆的心情,我们从源码角度分析这个问题,去找找实例代码中哪里创了太多对象。
线程池源码分析
以上的实例代码,就一个newFixedThreadPool和一个execute方法。首先,我们先来看一下newFixedThreadPool方法的源码
newFixedThreadPool源码
该段源码以及结合线程池特点,我们可以知道newFixedThreadPool:
- 核心线程数coreSize和最大线程数maximumPoolSize大小一样,都是nThreads。
- 空闲时间为0,即keepAliveTime为0
- 阻塞队列为无参构造的LinkedBlockingQueue
线程池特点了解不是很清楚的朋友,可以看我这篇文章,面试必备:Java线程池解析
接下来,我们再来看看线程池执行方法execute的源码。
线程池执行方法execute的源码
execute的源码以及相关解释如下:
纵观以上代码,我们可以发现就addWorker 以及workQueue.offer(command)可能在创建对象。那我们先分析addWorker方法。
addWorker源码分析
addWorker源码以及相关解释如下
addWorker执行流程:
大概就是判断线程池状态是否OK,如果OK,在判断当前工作中的线程数量是否满足(小于coreSize/maximumPoolSize),如果不满足,不添加,如果满足,就将执行任务添加到工作集合workers,,并启动执行该线程。
再看一下workers的类型:
workers是一个HashSet集合,它由coreSize/maximumPoolSize控制着,那么addWorker方法会导致OOM?结合实例代码demo,coreSize=maximumPoolSize=10,如果超过10,不会再添加到workers了,所以它不是导致newFixedThreadPool内存飙升的原因。那么,问题应该就在于workQueue.offer(command) 方法了。为了让整个流程清晰,我们画一下execute执行的流程图。
线程池执行方法execute的流程
根据以上execute以及addWork源码分析,我们把流程图画出来:
- 提交一个任务command,线程池里存活的核心线程数小于线程数corePoolSize时,调用addWorker方法,线程池会创建一个核心线程去处理提交的任务。
- 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
- 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
- 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理 。
看完execute的执行流程,我猜测,内存飙升问题就是workQueue塞满了。接下来,进行阻塞队列源码分析,揭开内存飙升问题的神秘面纱。
阻塞队列源码分析
回到newFixedThreadPool构造函数,发现阻塞队列就是LinkedBlockingQueue,而且是个无参的LinkedBlockingQueue队列。OK,那我们直接分析LinkedBlockingQueue源码。
LinkedBlockingQueue类图
由类图可以看到:
- LinkedBlockingQueue 是使用单向链表实现的,其有两个 Node,分别用来存放首、尾节点, 并且还有一个初始值为 0 的原子变量 count,用来记录 队列元素个数。
- 另外还有两个 ReentrantLock 的实例,分别用来控制元素入队和出队的原 子性,其中 takeLock 用来控制同时只有一个线程可以从队列头获取元素,其他线程必须 等待, putLock 控制同时只能有一个线程可以获取锁,在队列尾部添加元素,其他线程必 须等待。
- 另外, notEmpty 和 notFull 是条件变量,它们内部都有一个条件队列用来存放进 队和出队时被阻塞的线程,其实这是生产者一消费者模型。
LinkedBlockingQueue无参构造函数
LinkedBlockingQueue无参构造函数,默认构造Integer.MAX_VALUE(那么大)的链表,看到这里,你回想一下execute流程,是不是阻塞队列一直不会满了,这队列来者不拒,把所有阻塞任务收于麾下。。。是不是内存飙升问题水落石出啦。
LinkedBlockingQueue的offer函数
线程池中,插入队列用了offer方法,我们来看一下阻塞队列LinkedBlockingQueue的offer骚操作吧
offer操作向队列尾部插入一个元素,如果队列中有空闲则插入成功后返回 true,如果队列己满 则丢弃当前元素然后返回 false。如果 e 元素为 null 则抛出 Nul!PointerException 异常。另外, 该方法是非阻塞的。
内存飙升问题结果揭晓
newFixedThreadPool线程池的核心线程数是固定的,它使用了近乎于无界的LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。
参考与感谢
- 《Java并发编程之美》
- 面试必备:Java线程池解析
文章转载自公众号: 捡田螺的小男孩
