作者:何甜甜在吗

juejin.im/post/5dc41c165188257bad4d9e69

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

写在前面

首先感谢大家在盖楼的间隙阅读本篇文章,通过阅读本篇文章你将了解到:

  • 线程池的定义

  • Executors创建线程池的几种方式

  • ThreadPoolExecutor对象

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

  • Executors创建返回ThreadPoolExecutor对象

  • OOM异常测试

  • 如何定义线程池参数

如果只想知道原因可以直接拉到总结那

线程池的定义

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

  • 减少资源创建 => 减少内存开销,创建线程占用内存

  • 降低系统开销 => 创建线程需要时间,会延迟处理的请求

  • 提高稳定稳定性 => 避免无限创建线程引起的OutOfMemoryError【简称OOM】

Executors创建线程池的方式

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

  • 创建返回ThreadPoolExecutor对象

  • 创建返回ScheduleThreadPoolExecutor对象

  • 创建返回ForkJoinPool对象

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

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 => 线程池对拒绝任务的处理策略

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

执行逻辑说明:

  • 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

  • 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

  • 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

  • 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

Executors创建返回ThreadPoolExecutor对象

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

  • Executors#newCachedThreadPool => 创建可缓存的线程池

  • Executors#newSingleThreadExecutor => 创建单线程的线程池

  • Executors#newFixedThreadPool => 创建固定长度的线程池

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,可以认为最大线程数是无限的

  • keepAliveTime => 60L

  • unit => 秒

  • workQueue => SynchronousQueue

当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。

对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常

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,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 秒

  • workQueue => LinkedBlockingQueue

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

Executors#newFixedThreadPool方法

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

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

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 秒

  • workQueue => LinkedBlockingQueue

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

总结:

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

  • CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常

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

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错误

另外两个线程池就不做测试了,测试方法一致,只是创建的线程池不一样

如何定义线程池参数

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创建线程池?

    作者:何甜甜在吗 链接:https://juejin.im/post/5dc41c165188257bad4d9e69 来源:掘金 看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executo ...

  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. 安装iamp模块,编译报错configure: error: Cannot find imap library (libc-client.a). Please check your c-client installation.

    yum install libc-client-devel cd /root/lnmp1.0-full/php-5.3.17/ext/imap /usr/local/php/bin/phpize ./ ...

  2. sentos7网卡改名

    一.已经装好系统CentOS7修改网卡为eth0 1. 修改网卡配置文件[root@localhost ~]# mv /etc/sysconfig/network-scripts/ifcfg-ens3 ...

  3. [19/09/18-星期三] Python中的序列

    一. # 第四章 序列(视频58-76) ## 列表(list) - 列表是Python中的一个对象 - 对象(object)就是内存中专门用来存储数据的一块区域 - 之前我们学习的对象,像数值,它只 ...

  4. 打印页面内容,<input>不好使,用<textarea> 代替

    <textarea class="sld-textarea" onchange="changeTextareaValue(this)">123< ...

  5. centos6配置本地yum源

    在无法访问外网时,yum安装软件会失败,这时候可以配置yum源为本地的镜像iso来解决这个问题 1. 使用Xftp上传iso镜像文件到服务器 2. 使用如下命令新建挂载点并挂载 sudo mkdir ...

  6. 编写的Java第一个程序

    没什么好介绍的,嘻嘻 package head; public class ee { public static void main(String[] args) { System.out.print ...

  7. mysql基础知识和pymysql

    一.视图 视图是指计算机数据库中的视图,是一个虚拟表,其内容由查询定义.同真实的表一样,视图包含一系列带有名称的列和行数据.但是,视图并不在数据库中以存储的数据值集形式存在.行和列数据来自由定义视图的 ...

  8. 牛客练习赛14 E - 无向图中的最短距离 (bfs+bitset)

    一个链接:https://ac.nowcoder.com/acm/contest/82/E来源:牛客网 无向图中的最短距离 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 262144 ...

  9. flask框架上下文

    上下文 上下文:相当于一个容器,保存了 Flask 程序运行过程中的一些信息. Flask中有两种上下文,请求上下文和应用上下文 请求上下文(request context) 在 flask 中,可以 ...

  10. 【学习】013 Servlet、Cookie、Session的简述

    Servlet核心内容 Sevlet的生命周期(重点) Servlet重要的四个生命周期方法 构造方法: 创建servlet对象的时候调用.默认情况下,第一次访问servlet的时候创建servlet ...