作者:何甜甜在吗

链接:https://juejin.im/post/5dc41c165188257bad4d9e69

来源:掘金

  看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,通过源码分析禁用的原因。

1. 线程池的定义

  管理一组工作线程。通过线程池复用线程有以下几点优点:

  • 减少资源创建 => 减少内存开销,创建线程占用内存
  • 降低系统开销 => 创建线程需要时间,会延迟处理的请求
  • 提高稳定稳定性 => 避免无限创建线程引起的OutOfMemoryError【简称OOM】

2. Executors创建线程池的方式

  根据返回的对象类型创建线程池可以分为三类:

  • 创建返回ThreadPoolExecutor对象
  • 创建返回ScheduleThreadPoolExecutor对象
  • 创建返回ForkJoinPool对象

  本文只讨论创建返回ThreadPoolExecutor对象。

3. ThreadPoolExecutor对象

  在介绍Executors创建线程池方法前先介绍一下ThreadPoolExecutor,因为这些创建线程池的静态方法都是返回ThreadPoolExecutor对象,和我们手动创建ThreadPoolExecutor对象的区别就是我们不需要自己传构造函数的参数。ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

  构造函数参数说明:

  • corePoolSize => 线程池核心线程数量
  • maximumPoolSize => 线程池最大数量
  • keepAliveTime => 空闲线程存活时间
  • unit => 时间单位
  • workQueue => 线程池所使用的缓冲队列
  • threadFactory => 线程池创建线程使用的工厂
  • handler => 线程池对拒绝任务的处理策略

4. 线程池执行任务逻辑和线程池参数的关系

  执行逻辑说明:

  • 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务;
  • 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中;
  • 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务;
  • 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关;

5. Executors创建返回ThreadPoolExecutor对象

  Executors创建返回ThreadPoolExecutor对象的方法共有三种:

  • Executors#newCachedThreadPool => 创建可缓存的线程池
  • Executors#newSingleThreadExecutor => 创建单线程的线程池
  • Executors#newFixedThreadPool => 创建固定长度的线程池

5.1 Executors#newCachedThreadPool方法

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

  CachedThreadPool是一个根据需要创建新线程的线程池

  • corePoolSize => 0,核心线程池的数量为0
  • maximumPoolSize => Integer.MAX_VALUE,线程池最大数量为Integer.MAX_VALUE,可以认为可以无限创建线程
  • keepAliveTime => 60L
  • unit => 秒
  • workQueue => SynchronousQueue

  当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常。

5.2 Executors#newSingleThreadExecutor方法

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

  SingleThreadExecutor是单线程线程池,只有一个核心线程

  • corePoolSize => 1,核心线程池的数量为1
  • maximumPoolSize => 1,线程池最大数量为1,即最多只可以创建一个线程,唯一的线程就是核心线程
  • keepAliveTime => 0L
  • unit => 毫秒
  • workQueue => LinkedBlockingQueue

  当一个任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize和keepAliveTime参数将无效,压根就不会创建非核心线程

5.3 Executors#newFixedThreadPool方法

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

  FixedThreadPool是固定核心线程的线程池,固定核心线程数由用户传入

  • corePoolSize => nThreads,核心线程池的数量为1
  • maximumPoolSize => nThreads,线程池最大数量为nThreads,即最多只可以创建nThreads个线程
  • keepAliveTime => 0L
  • unit => 毫秒
  • workQueue => LinkedBlockingQueue

  它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常

6. 总结

  • FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常
  • CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常

  这就是为什么禁止使用Executors去创建线程池,而是推荐自己去创建ThreadPoolExecutor的原因。

7. OOM异常测试

  理论上会出现OOM异常,必须测试一波验证之前的说法: 测试类:TaskTest.java

public class TaskTest {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
int i = 0;
while (true) {
es.submit(new Task(i++));
}
}
}

  使用Executors创建的CachedThreadPool,往线程池中无限添加线程。在启动测试类之前先将JVM内存调整小一点,不然很容易将电脑跑出问题【别问我为什么知道,是铁憨憨甜没错了!!!】,在idea里:Run -> Edit Configurations

  JVM参数说明:

  • -Xms10M => Java Heap内存初始化值
  • -Xmx10M => Java Heap内存最大值

  运行结果:

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Disconnected from the target VM, address: '127.0.0.1:60416', transport: 'socket'

  创建到3w多个线程的时候开始报OOM错误。另外两个线程池就不做测试了,测试方法一致,只是创建的线程池不一样。

8. 如何定义线程池参数

  CPU密集型 => 线程池的大小推荐为CPU数量 + 1,CPU数量可以根据Runtime.availableProcessors方法获取

  IO密集型 => CPU数量 * CPU利用率 * (1 + 线程等待时间/线程CPU时间)

  混合型 => 将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整

  阻塞队列 => 推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生

  拒绝策略 => 默认采用的是AbortPolicy拒绝策略,直接在程序中抛出RejectedExecutionException异常【因为是运行时异常,不强制catch】,这种处理方式不够优雅。处理拒绝策略有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略
  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低
  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可
  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的

  如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

[转]为什么阿里巴巴要禁用Executors创建线程池?的更多相关文章

  1. 为什么阿里巴巴要禁用Executors创建线程池?

    作者:何甜甜在吗 juejin.im/post/5dc41c165188257bad4d9e69 看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadP ...

  2. 为什么尽量不要使用Executors创建线程池

    看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,通过源码分析禁用的原因. 线程池的优点 管理一组工作线程,通过线程池 ...

  3. 阿里不允许使用 Executors 创建线程池!那怎么使用,怎么监控?

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 五常大米好吃! 哈哈哈,是不你总买五常大米,其实五常和榆树是挨着的,榆树大米也好吃, ...

  4. Executors创建线程池的几种方式以及使用

    Java通过Executors提供四种线程池,分别为:   1.newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.   ...

  5. 【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?

    前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedT ...

  6. JAVA中创建线程池的五种方法及比较

    之前写过JAVA中创建线程的三种方法及比较.这次来说说线程池. JAVA中创建线程池主要有两类方法,一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用.另一类是通过Thr ...

  7. Java 并发:Executors 和线程池

    让我们开始来从入门了解一下 Java 的并发编程. 本文主要介绍如何开始创建线程以及管理线程池,在 Java 语言中,一个最简单的线程如下代码所示: Runnable runnable = new R ...

  8. javade多任务处理之Executors框架(线程池)实现的内置几种方式与两种基本自定义方式

    一 Executors框架(线程池) 主要是解决开发人员进行线程的有效控制,原理可以看jdk源码,主要是由java.uitl.concurrent.ThreadPoolExecutor类实现的,这里只 ...

  9. java线程池之一:创建线程池的方法

    在Java开发过程中经常需要用到线程,为了减少资源的开销,提高系统性能,Java提供了线程池,即事先创建好线程,如果需要使用从池中取即可,Java中创建线程池有以下的方式, 1.使用ThreadPoo ...

随机推荐

  1. Milk Pumping

    今天第一次正式打个人定位赛,还是太菜,这题连枚举加最短路都没想到,显然菜是原罪. 题面: : 题解:其实方法很多,千万别浪到网络流用dinic求最大网络流求的最小费用,这题不一样.最大流/最小费用 不 ...

  2. SaaS 系统架构,Spring Boot 动态数据源实现!

    这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会. 在网上找了很多关于SaaS的资料 ...

  3. java 基本语法(四) 流程控制(一) 分支结构

    if(条件表达式){ 执行表达式} 结构二:二选一if(条件表达式){ 执行表达式1}else{ 执行表达式2} 结构三:n选一if(条件表达式){ 执行表达式1}else if(条件表达式){ 执行 ...

  4. 数据分析05 /pandas的高级操作

    数据分析05 /pandas的高级操作 目录 数据分析05 /pandas的高级操作 1. 替换操作 2. 映射操作 3. 运算工具 4. 映射索引 / 更改之前索引 5. 排序实现的随机抽样/打乱表 ...

  5. 数据可视化之DAX篇(三) 认识DAX中的表函数和值函数

    https://zhuanlan.zhihu.com/p/64421003 学习 DAX 的过程中,会遇到各种坑,刚开始甚至无法写出一个正确的度量值,总是提示错误.其实很多原因都是不理解 DAX 函数 ...

  6. redis(十二):Redis 集合(Set)

    Redis 集合(Set) Redis 的 Set 是 String 类型的无序集合.集合成员是唯一的,这就意味着集合中不能出现重复的数据. Redis 中集合是通过哈希表实现的,所以添加,删除,查找 ...

  7. saver 的保存与恢复

    模型保存,先要创建一个Saver对象:saver=tf.train.Saver(), max_to_keep 是用来设置保存模型的个数,默认为5,即保存最近的五个模型,saver=tf.train.S ...

  8. Unity-内存

    editor 和runtime的内存管理分开的 unity检测不到native内存容量 如c++,lua 一个asset一个ab的问题在于 每个asset都有对应的文件头,并不划算 IL2CPP抛弃了 ...

  9. 初级软件工程师怎么走向BATJ?——献给迷茫中的测试人

    软件测试,邀你同行.你好,我是爱码小哥. 又是一个深夜,打开手机备忘录,想记录一些东西,本人比较静的一个人,所以经常会去 IT行业的贴吧论坛交流一下,逛知乎,论坛,社区你就会发现大量这样的帖子,都会出 ...

  10. [Qt插件]-01Qt插件&&提升部件(自定义控件)

    本篇的文档为:How to Create Qt Plugins https://doc.qt.io/qt-5/plugins-howto.html   前言 Qt插件(Qt Plugin)就是一个共享 ...