前言

使用无界队列的线程池会导致内存飙升吗?面试官经常会问这个问题,本文将基于源码,去分析newFixedThreadPool线程池导致的内存飙升问题,希望能加深大家的理解。

(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

内存飙升问题复现

实例代码

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}

配置Jvm参数

IDE指定JVM参数:-Xmx8m -Xms8m :

执行结果

run以上代码,会抛出OOM:

JVM OOM问题一般是创建太多对象,同时GC 垃圾来不及回收导致的,那么什么原因导致线程池的OOM呢?带着发现新大陆的心情,我们从源码角度分析这个问题,去找找实例代码中哪里创了太多对象。

线程池源码分析

以上的实例代码,就一个newFixedThreadPool和一个execute方法。首先,我们先来看一下newFixedThreadPool方法的源码

newFixedThreadPool源码

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

该段源码以及结合线程池特点,我们可以知道newFixedThreadPool:

  • 核心线程数coreSize和最大线程数maximumPoolSize大小一样,都是nThreads。
  • 空闲时间为0,即keepAliveTime为0
  • 阻塞队列为无参构造的LinkedBlockingQueue

线程池特点了解不是很清楚的朋友,可以看我这篇文章,面试必备:Java线程池解析

接下来,我们再来看看线程池执行方法execute的源码。

线程池执行方法execute的源码

execute的源码以及相关解释如下:

 public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { //步骤一:判断当前正在工作的线程是否比核心线程数量小
if (addWorker(command, true)) // 以核心线程的身份,添加到工作集合
return;
c = ctl.get();
}
//步骤二:不满足步骤一,线程池还在RUNNING状态,阻塞队列也没满的情况下,把执行任务添加到阻塞队列workQueue。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//来个double check ,检查线程池是否突然被关闭
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//步骤三:如果阻塞队列也满了,执行任务以非核心线程的身份,添加到工作集合
else if (!addWorker(command, false))
reject(command);
}

纵观以上代码,我们可以发现就addWorker 以及workQueue.offer(command) 可能在创建对象。那我们先分析addWorker方法。

addWorker源码分析

addWorker源码以及相关解释如下

private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//获取当前线程池的状态
int rs = runStateOf(c);
//如果线程池状态是STOP,TIDYING,TERMINATED状态的话,则会返回false。
// 如果现在状态是SHUTDOWN,但是firstTask不为空或者workQueue为空的话,那么直接返回false
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//自旋
for (;;) {
//获取当前工作线程的数量
int wc = workerCountOf(c);
//判断线程数量是否符合要求,如果要创建的是核心工作线程,判断当前工作线程数量是否已经超过coreSize,
// 如果要创建的是非核心线程,判断当前工作线程数量是否超过maximumPoolSize,是的话就返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//如果线程数量符合要求,就通过CAS算法,将WorkerCount加1,成功就跳出retry自旋
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
retry inner loop
}
}
//线程启动标志
boolean workerStarted = false;
//线程添加进集合workers标志
boolean workerAdded = false;
Worker w = null;
try {
//由(Runnable 构造Worker对象
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//获取线程池的重入锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//获取线程池状态
int rs = runStateOf(ctl.get());
//如果状态满足,将Worker对象添加到workers集合
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//启动Worker中的线程开始执行任务
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//线程启动失败,执行addWorkerFailed方法
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

addWorker执行流程

大概就是判断线程池状态是否OK,如果OK,在判断当前工作中的线程数量是否满足(小于coreSize/maximumPoolSize),如果不满足,不添加,如果满足,就将执行任务添加到工作集合workers,,并启动执行该线程。

再看一下workers的类型:

/**

  • Set containing all worker threads in pool. Accessed only when
  • holding mainLock.
  • */
  • private final HashSet workers = new HashSet();

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无参构造函数

public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}

LinkedBlockingQueue无参构造函数,默认构造Integer.MAX_VALUE(那么大) 的链表,看到这里,你回想一下execute流程,是不是阻塞队列一直不会满了,这队列来者不拒,把所有阻塞任务收于麾下。。。是不是内存飙升问题水落石出啦。

LinkedBlockingQueue的offer函数

线程池中,插入队列用了offer方法,我们来看一下阻塞队列LinkedBlockingQueue的offer骚操作吧

public boolean offer(E e) {
//为空元素则抛出空指针异常
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
//如采当前队列满则丢弃将要放入的元素, 然后返回false
if (count.get() == capacity)
return false;
int c = -1;
//构造新节点,获取putLock独占锁
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//如采队列不满则进队列,并递增元素计数
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
//新元素入队后队列还有空闲空间,则
唤醒 notFull 的条件队列中一条阻塞线程
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//释放锁
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}

offer操作向队列尾部插入一个元素,如果队列中有空闲则插入成功后返回 true,如果队列己满 则丢弃当前元素然后返回 false。 如果 e 元素为 null 则抛出 Nul!PointerException 异常。另外, 该方法是非阻塞的。

内存飙升问题结果揭晓

newFixedThreadPool线程池的核心线程数是固定的,它使用了近乎于无界的LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。

源码角度分析-newFixedThreadPool线程池导致的内存飙升问题的更多相关文章

  1. 硬核干货:4W字从源码上分析JUC线程池ThreadPoolExecutor的实现原理

    前提 很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章.之前在分析扩展线程池实现可回调的Future时候曾经提到并发 ...

  2. Java并发包源码学习系列:线程池ThreadPoolExecutor源码解析

    目录 ThreadPoolExecutor概述 线程池解决的优点 线程池处理流程 创建线程池 重要常量及字段 线程池的五种状态及转换 ThreadPoolExecutor构造参数及参数意义 Work类 ...

  3. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

  4. Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析

    目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...

  5. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  6. 从源码角度分析 MyBatis 工作原理

    一.MyBatis 完整示例 这里,我将以一个入门级的示例来演示 MyBatis 是如何工作的. 注:本文后面章节中的原理.源码部分也将基于这个示例来进行讲解.完整示例源码地址 1.1. 数据库准备 ...

  7. Android的Message Pool是什么——源码角度分析

    原文地址: http://blog.csdn.net/xplee0576/article/details/46875555 Android中,我们在线程之间通信传递通常采用Android的消息机制,而 ...

  8. 【原创】源码角度分析Android的消息机制系列(五)——Looper的工作原理

    ι 版权声明:本文为博主原创文章,未经博主允许不得转载. Looper在Android的消息机制中就是用来进行消息循环的.它会不停地循环,去MessageQueue中查看是否有新消息,如果有消息就立刻 ...

  9. 源码解读 TDengine 中线程池的实现

    这篇文章中提到了 tsched 的源码可以一读,所以去阅读了一下,总共220来行. 1. 阅读前工作 通过上文了解到这段程序实现的是一个任务队列,同时带有线程池.这段程序是计算机操作系统里经典的con ...

随机推荐

  1. 区块链学习笔记:D02 区块链的技术发展历史和趋势

    对于区块链的技术发展历史,其实在我的印象中也就对比特币有所了解,也听过什么火币之类的玩意,但是具体是什么.怎么运作的就不清楚了... 这次的内容首先是讲解了区块链的技术演进,一张图一目了然,虽然里面涉 ...

  2. sbt assembly a fat jar for spark-submit cluster model

    在用spark-submit提交作业时,用sbt package打包好的jar程序,可以很好的运行在client模式,当在cluster模式, 一直报错:Exception in thread &qu ...

  3. asp.net core中间件工作原理

    不少刚学习.net core朋友对中间件的概念一直分不清楚,到底StartUp下的Configure方法是在做什么? public void Configure(IApplicationBuilder ...

  4. leetcode字节跳动专题(持续更新)

    挑战字符串 无重复字符的最长子串 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: "abcabcbb" 输出: 3 解释: 因为无重复字符的最 ...

  5. 洛谷 题解 P2117 【小Z的矩阵】

    这题这么无聊,亏我还用了读入输出优化... 关键在于,这还是道黄题QWQ 掀桌而起 (╯‵□′)╯︵┻━┻ 显而易见,在i != j的情况下,a[i][j] + a[j][i]和a[j][i] + a ...

  6. 一份详细的 Matplotlib入门指导

    hMatplotlib是最受欢迎的二维图形库,但有时我们很难做到得心应手的去使用. 如何更改图例上的标签名称? 如何设置刻度线? 如何将比例更改为对数? 如何在我的情节中添加注释和箭头? 如何在我的图 ...

  7. tensorflow SavedModelBuilder用法

    训练代码: # coding: utf-8 from __future__ import print_function from __future__ import division import t ...

  8. Orleans的入门教程

    Orleans的入门教程  官方Hello World 地址 https://github.com/dotnet/orleans/tree/master/Samples/2.0/HelloWorld ...

  9. Socket无法通过防火墙的问题

    无论是配好端口还是例外的应用程序都不行 更改本地终结点为 socket.Bind()); IPAddress.Any 不要使用127.0.0.1 不要使用127.0.0.1 不要使用127.0.0.1

  10. JS基础-事件

    事件机制 事件触发三阶段 事件触发有三个阶段: window 往事件触发处传播,遇到注册的捕获事件会触发 传播到事件触发处时触发注册的事件 从事件触发处往 window 传播,遇到注册的冒泡事件会触发 ...