Java中线程池,你真的会用吗?ExecutorService ThreadPoolExcutor
原文:https://www.hollischuang.com/archives/2888
在《深入源码分析Java线程池的实现原理》这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理。
在文中有这样一段描述:
可以通过Executors静态工厂构建线程池,但一般不建议这样使用。
关于这个问题,在那篇文章中并没有深入的展开。作者之所以这么说,是因为这种创建线程池的方式有很大的隐患,稍有不慎就有可能导致线上故障,如:一次Java线程池误用引发的血案和总结( https://zhuanlan.zhihu.com/p/32867181 )
本文我们就来围绕这个问题来分析一下为什么JDK自身提供的构建线程池的方式并不建议使用?到底应该如何创建一个线程池呢?
Executors
Executors 是一个Java中的工具类。提供工厂方法来创建不同类型的线程池。

从上图中也可以看出,Executors的创建线程池的方法,创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个:
newFiexedThreadPool(int Threads):创建固定数目线程的线程池。
newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
newSingleThreadExecutor()创建一个单线程化的Executor。
newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
类看起来功能还是比较强大的,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,如:
ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;
即可创建一个固定大小的线程池。
但是为什么我说不建议大家使用这个类来创建线程池呢?
我提到的是『不建议』,但是在阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors创建线程池。

Executors存在什么问题
在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors?
我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况。
/**
* @author Hollis
*/
public class ExecutorsDemo {
private static ExecutorService executor = Executors.newFixedThreadPool(15);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(new SubThread());
}
}
}
class SubThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
}
}
通过指定JVM参数:-Xmx8m -Xms8m 运行以上代码,会抛出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
以上代码指出,ExecutorsDemo.java的第16行,就是代码中的executor.execute(new SubThread());。
Executors为什么存在缺陷
通过上面的例子,我们知道了Executors创建的线程池存在OOM的风险,那么到底是什么原因导致的呢?我们需要深入Executors的源码来分析一下。
其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致OOM的其实是LinkedBlockingQueue.offer方法。
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
如果读者翻看代码的话,也可以发现,其实底层确实是通过LinkedBlockingQueue实现的:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。
Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。
而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。
创建线程池的正确姿势
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。
作者推荐使用guava提供的ThreadFactoryBuilder来创建线程池。
public class ExecutorsDemo {
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
pool.execute(new SubThread());
}
}
}
通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。
Java中线程池,你真的会用吗?ExecutorService ThreadPoolExcutor的更多相关文章
- Java中线程池,你真的会用吗?
在<深入源码分析Java线程池的实现原理>这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理. 在文中有这样一段描述: 可以通过Executors静态工厂构建线程池,但一般不建 ...
- 沉淀再出发:java中线程池解析
沉淀再出发:java中线程池解析 一.前言 在多线程执行的环境之中,如果线程执行的时间短但是启动的线程又非常多,线程运转的时间基本上浪费在了创建和销毁上面,因此有没有一种方式能够让一个线程执行完自己的 ...
- Java中线程池的学习
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程 ...
- java中线程池的使用方法
1 引入线程池的原因 由于线程的生命周期中包括创建.就绪.运行.阻塞.销毁阶段,当我们待处理的任务数目较小时,我们可以自己创建几个线程来处理相应的任务,但当有大量的任务时,由于创建.销毁线程需要很大的 ...
- Java中线程池的实现原理
知识点总结 ---------------------------------------------------------------------------------------------- ...
- Java中线程池的实现原理-求职必备
jdk1.5引入Executor线程池框架,通过它把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行.被哪个线程执行,以及什么时候执行. 初始化线程池(4种) ...
- JAVA中线程池的简单使用
比如现在有10个线程,但每次只想运行3个线程,当这3个线程中的任何一个运行完后,第4个线程接着补上.这种情况可以使用线程池来解决,线程池用起来也相当的简单,不信,你看: package com.dem ...
- java中线程池的几种实现方式
1.线程池简介: 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项任务所需时间为:T1 创建 ...
- 第九章 Java中线程池
Java中的线程池是运用场景最多的并发框架,几乎所有需求异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 降低资源消耗:通过重复利用已创建的线程降低线程创建和 ...
随机推荐
- TransactionScope处理分布式事物时提示"事务已被隐式或显式提交,或已终止"
在连接字符串中加入"Enlist=false",问题就这样解决了. ConnectionString = "Data Source=.;Initial Catalog=c ...
- [转]Mathjax语法总结
链接地址:https://blog.csdn.net/ajacker/article/details/80301378
- html中测试div、ul和li、table排列多个块
前面有三篇博文测试了这三种方式,一.相关博文:LODOP问答部分链接.该文用的是div定位,用的是所有小div相对于大div进行定位,大的div设置relative定位,小的设置absolute相对于 ...
- html5传感器
html5传感器(注意苹果和安卓方向是相反的 回调函数触发的速度苹果要快很多 设置坐标不要设置在回调函数里)以下是代码<pre><!DOCTYPE html><html ...
- 资源池-数据库连接池简单实现-JAVA版本
转载:https://www.jianshu.com/p/381c86bdbff6 看了看druid和dbcp2的原码,发现他们都有各自存储连接的方式. druid : private volatil ...
- webbench网站测压工具源码分析
/* * (C) Radim Kolar 1997-2004 * This is free software, see GNU Public License version 2 for * detai ...
- LocalStack和Local对象实现栈的管理
flask里面有两个重要的类Local和LocalStack 输入from flask import globals 左键+ctrl点globals进入源码,进去后找57行 flask只会实例化出这两 ...
- 2019-07-02 python流程控制
今天的知识点包括:if / while / for 为什么要有if判断:判断指的是判断事物的对错,真假,想让计算机像人一样去工作.思考,那么计算机也应该有判断事物的对错的能力,那么就要用到if判断语句 ...
- Python之路【第十二篇】:Python面向对象高级
一.反射 1 什么是反射 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问.检测和修改它本身状态或行为的一种能力(自省).这一概念的提出很快引发了计算机科学领域关于应用反射性的研究 ...
- STM32串口复用关系&printf重定义
串口复用 什么是普通功能输入输出?普通功能输入输出类似于:大厅<—>门<—>室外的关系,大厅只需要经过大门即可到室外,从室外经过门也可以到达大厅. 什么是复用工功能输入输出?复 ...