面试突击32:为什么创建线程池一定要用ThreadPoolExecutor?
在 Java 语言中,并发编程都是依靠线程池完成的,而线程池的创建方式又有很多,但从大的分类来说,线程池的创建总共分为两大类:手动方式使用 ThreadPoolExecutor 创建线程池和使用 Executors 执行器自动创建线程池。
那究竟要使用哪种方式来创建线程池呢?我们今天就来详细的聊一聊。
先说结论
在 Java 语言中,一定要使用 ThreadPoolExecutor 手动的方式来创建线程池,因为这种方式可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,并且可以规避资源耗尽的风险。
OOM风险演示
假如我们使用了 Executors 执行器自动创建线程池的方式来创建线程池,那么就会存现线程溢出的风险,以 CachedThreadPool 为例我们来演示一下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorExample {
static class OOMClass {
// 创建 1MB 大小的变量(1M = 1024KB = 1024*1024Byte)
private byte[] data_byte = new byte[1 * 1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
// 使用执行器自动创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
List<Object> list = new ArrayList<>();
// 添加任务
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
// 定时添加
try {
Thread.sleep(finalI * 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将 1M 对象添加到集合
OOMClass oomClass = new OOMClass();
list.add(oomClass);
System.out.println("执行任务:" + finalI);
}
});
}
}
}
第 2 步将 Idea 中 JVM 最大运行内存设置为 10M(设置此值主要是为了方便演示),如下图所示:

以上程序的执行结果如下图所示:

从上述结果可以看出,当线程执行了 7 次之后就开始出现 OutOfMemoryError 内存溢出的异常了。
内存溢出原因分析
想要了解内存溢出的原因,我们需要查看 CachedThreadPool 实现的细节,它的源码如下图所示:

构造函数的第 2 个参数被设置成了 Integer.MAX_VALUE,这个参数的含义是最大线程数,所以由于 CachedThreadPool 并不限制线程的数量,当任务数量特别多的时候,就会创建非常多的线程。而上面的 OOM 示例,每个线程至少要消耗 1M 大小的内存,加上 JDK 系统类的加载也要占用一部分的内存,所以当总的运行内存大于 10M 的时候,就出现内存溢出的问题了。
使用ThreadPoolExecutor来改进
接下来我们使用 ThreadPoolExecutor 来改进一下 OOM 的问题,我们使用 ThreadPoolExecutor 手动创建线程池的方式,创建一个最大线程数为 2,最多可存储 2 个任务的线程池,并且设置线程池的拒绝策略为忽略新任务,这样就能保证线程池的运行内存大小不会超过 10M 了,实现代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* ThreadPoolExecutor 演示示例
*/
public class ThreadPoolExecutorExample {
static class OOMClass {
// 创建 1MB 大小的变量(1M = 1024KB = 1024*1024Byte)
private byte[] data_byte = new byte[1 * 1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
// 手动创建线程池,最大线程数 2,最多存储 2 个任务,其他任务会被忽略
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略:忽略任务
List<Object> list = new ArrayList<>();
// 添加任务
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
// 定时添加
try {
Thread.sleep(finalI * 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将 1m 对象添加到集合
OOMClass oomClass = new OOMClass();
list.add(oomClass);
System.out.println("执行任务:" + finalI);
}
});
}
// 关闭线程池
threadPool.shutdown();
// 检测线程池的任务执行完
while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {
System.out.println("线程池中还有任务在处理");
}
}
}
以上程序的执行结果如下图所示:

从上述结果可以看出,线程池从开始执行到执行结束都没有出现 OOM 的异常,这就是手动创建线程池的优势。
其他创建线程池的问题
除了 CachedThreadPool 线程池之外,其他使用 Executors 自动创建线程池的方式,也存在着其他一些问题,比如 FixedThreadPool 它的实现源码如下:

而默认情况下任务队列 LinkedBlockingQueue 的存储容量是 Integer.MAX_VALUE,也是趋向于无限大,如下图所示:

这样就也会造成,因为线程池的任务过多而导致的内存溢出问题。其他几个使用 Executors 自动创建线程池的方式也存在此问题,这里就不一一演示了。
总结
线程池的创建方式总共分为两大类:手动使用 ThreadPoolExecutor 创建线程池和自动使用 Executors 执行器创建线程池的方式。其中使用 Executors 自动创建线程的方式,因为线程个数或者任务个数不可控,可能会导致内存溢出的风险,所以在创建线程池时,建议使用 ThreadPoolExecutor 的方式来创建。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java面试真题解析
面试突击32:为什么创建线程池一定要用ThreadPoolExecutor?的更多相关文章
- 【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?
前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedT ...
- 阿里不允许使用 Executors 创建线程池!那怎么使用,怎么监控?
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 五常大米好吃! 哈哈哈,是不你总买五常大米,其实五常和榆树是挨着的,榆树大米也好吃, ...
- Flash图解线程池 | 阿里巴巴面试官希望问的线程池到底是什么?
前言 前几天小强去阿里巴巴面试Java岗,止步于二面. 他和我诉苦自己被虐的多惨多惨,特别是深挖线程和线程池的时候,居然被问到不知道如何作答. 对于他的遭遇,结合他过了一面的那个嘚瑟样,我深表同情(加 ...
- Python并发编程之消息队列补充及如何创建线程池(六)
大家好,并发编程 进入第六篇. 在第四章,讲消息通信时,我们学到了Queue消息队列的一些基本使用.昨天我在准备如何创建线程池这一章节的时候,发现对Queue消息队列的讲解有一些遗漏的知识点,而这些知 ...
- Executors创建线程池的几种方式以及使用
Java通过Executors提供四种线程池,分别为: 1.newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程. ...
- java线程池之一:创建线程池的方法
在Java开发过程中经常需要用到线程,为了减少资源的开销,提高系统性能,Java提供了线程池,即事先创建好线程,如果需要使用从池中取即可,Java中创建线程池有以下的方式, 1.使用ThreadPoo ...
- 你创建线程池最好分为两种线程池,io密集型线程池,或者cpu密集型线程池
你创建线程池最好分为两种线程池,io密集型线程池,或者cpu密集型线程池. 否则,如果只用一个线程池的话,不管是iO密集的线程,或者cpu消耗大的都放在同一个线程池的话,会发生线程池被撑满的情况
- 使用Runnable接口创建线程池
步骤: 创建线程池对象创建 Runnable 接口子类对象提交 Runnable 接口子类对象关闭线程池实例: class TaskRunnable implements Runnable{ @Ove ...
- 为什么阿里巴巴要禁用Executors创建线程池?
作者:何甜甜在吗 juejin.im/post/5dc41c165188257bad4d9e69 看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadP ...
随机推荐
- MySQL高级篇笔记
目录 MySQL体系结构 存储引擎特点 InnoDB底层文件 MyISAM底层文件 索引 慢查询日志 profile详情 explain执行计划 EXPLAIN 执行计划各字段含义: 索引使用 最左前 ...
- Note -「群论」学习笔记
目录 前置知识 群 置换 Burnside 引理与 Pólya 定理 概念引入 引例 轨道-稳定子(Orbit-Stabilizer)定理 证明 Burnside 引理 证明 Pólya 定理 证明 ...
- Solution -「洛谷 P4389」付公主的背包
\(\mathcal{Description}\) Link. 容量为 \(n\),\(m\) 种物品的无限背包,求凑出每种容量的方案数,对 \(998244353\) 取模. \(n,m ...
- Solution -「Tenka1 2019 D」Three Colors
\(\mathcal{Description}\) Link. 给定 \(\{a_n\}\),把每个元素划分入可重集 \(R,G,B\) 中的恰好一个,求满足 \(\sum R,\sum G, ...
- c++ struct enum union加typedef与不加typedef
struct/enum/union加typedef与不加typedef 匿名结构体 struct { int a; int b; } v; // 这里表示定义了一个结构体的变量v,且结构体类型没有名字 ...
- 面试官灵魂三问:什么是SOA?什么是微服务?SOA和微服务有什么区别?
SOA SOA(Service-Oriented Architecture,面向服务的架构)是一种高层级的架构设计理念,可通过在网络上使用基于通用通信语言的服务接口,让软件组件可重复使用. 那么什么是 ...
- 基于nodejs的阿里云DDNS服务,支持多网卡绑定
前段几天写的,自用. 想着也许有和我一样需求的同学,所以开源发出来了. 提供阿里云的DDNS设置,支持多网卡多域名绑定. 特性 支持阿里云DDNS设置 支持针对多个线路多个网卡单独设置域名 支持IPv ...
- CMake 交叉编译
CMake 交叉编译 交叉编译就是说在平台 A (宿主机)上编译出可以在平台 B (目标机) 上运行的程序,比如在 x86 上编译 ARM 程序 要交叉编译首先要去下载目标平台的工具链,比如要编译 A ...
- 面试题(造火箭必备技能):请举例一个最有成就感的性能bug
当前,绝大部分招聘都有性能要求或者把其作为加分项(会性能优先),哪怕你不是面试的性能,面试的时候可能会问性能,所以大家才会有"面试造火箭,进去拧螺丝"的共鸣.至于企业为什么重视性能 ...
- 忘掉cmd.exe吧!选用优雅的控制台终端(ConsoleZ)
1.使用cmd的烦恼 嵌入式开发经常会用到命令行工具,Windows系统自带的command line工具(cmd.exe)的简陋程度不用说大家都深有体会.使用cmd.exe有如下几个主要的烦恼: 没 ...