Java线程池的了解使用—筑基篇
前言
Java中的线程池是一个很重要的概念,它的应用场景十分广泛,可以被广泛的用于高并发的处理场景。J.U.C提供的线程池:ThreadPoolExecutor类,可以帮助我们管理线程并方便地并行执行任务。因此了解并合理使用线程池非常重要。
本文对线程池采用 3W 的策略结合源码进行思考逐层分析,即是什么为什么怎么做。
什么是线程池
线程池的本质是对任务和线程的管理,做到了将任务和线程两者解耦。线程池对任务的管理可看作生产者消费者的关系,通过阻塞队列的存与取。阻塞队列缓存待执行的任务,工作线程从阻塞队列中获取任务。线程池对线程的管理,是结合线程池状态,已有线程的状态,核心线程数和最大线程数、阻塞队列状态做出增加、执行任务、回收、复用等操作,体现了享元模式和池化思想。
享元模式:
主要目的是实现对象的共享,运用共享技术有效地支持大量细粒度的对象,避免大量相类似的开销。当系统中对象多的时候可以减少内存的开销,通常与搭配工厂模式使用。
池化思想:
在多种使用对象的策略上,主张让使用的代价最小化。在重新创建对象的代价 远大于更换状态,复用对象的代价的前提下,将可以复用的对象放入池中待复用,以此降低使用的代价。
为什么要用线程池
线程池的优点,也是它为什么被流行使用的原因:
- 重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。
- 能有效控制线程池的最大并发数,能提供定时执行以及定间隔循环执行等功能。
- 线程池还提供了一种方法来约束和管理执行一组任务时消耗的资源(包括线程),避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
- 可维护一些基本统计信息,比如已完成任务的数量。
主要的缺点:
- 线程池的参数不存在完美的配置,高度依赖于开发者的经验,使用不当容易造成线上的危机
- 线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,业界并没有一些成熟的经验策略帮助开发人员参考。
怎么用线程池
先了解线程池的相关重要概念:
Core and maximum pool sizes
核心线程数以及最大线程数,这是构造一个线程池所必需的参数。
不同的搭配会有不同效果的线程池,也是线程池判断在运行任务前是否需创建新线程的重要依据。
ThreadFactory
线程工厂,这是构造一个线程池的参数。
提供线程的创建,如果构建线程池不指定ThreadFactory,则使用默认线程工厂,创建的线程默认进入同一个ThreadGroup和默认线程优先级。
Keep-alive times
存活时间,这是构造一个线程池的参数。
如果线程池当前有超过corePoolSize大小的线程,如果非核心线程的空闲时间超过了keepAliveTime,则被视为可回收的多余线程,被终止
Queuing
任务/阻塞队列,这是构造一个线程池的参数。
不同类型的阻塞队列可以构造出适合不同场景的线程池。最常见的四种线程池就有着不同类型的阻塞队列。
作为任务的缓冲停留区,线程池管理线程的机制核心之一。
生产者消费者模式的体现,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
- 在队列为空时,获取元素的线程会等待队列变为非空再尝试获取
- 当队列满时,存储元素的线程会等待队列可用再尝试存储

Rejected tasks
拒绝任务后的策略,这是构造一个线程池的参数
加入任务时,根据线程池当前状态是否停止销毁、线程数是否以及饱和,判断是否拒绝本次任务的加入。若拒绝任务就会执行拒绝任务后的策略。默认的拒绝后的策略是抛出运行期异常RejectedExecutionException。
On-demand construction
需求到达才创建,默认情况下,即使是核心线程最初也只有在新任务到达时才创建和启动,但是可以使用prestartCoreThread。如果使用非空队列构造池,可能需要预启动线程。
Hook methods
钩子方法
可重写的方法,beforeExecute(Runnable) ; afterExecute(Runnable,Throwable);terminated ,在执行每个任务之前和之后,线程池被完全终止后会被回调。可以用来执行特殊任务:重新初始化ThreadLocal变量、收集统计信息或添加日志条目。
最常见最常用的线程池
Executors类提供的也是最常见的线程池种类,配置,以及它们维护的阻塞队列类型,使用场景如下:
| 类型 | 核心线程数 | 最大线程数 | 阻塞队列 | 说明/使用场景 |
|---|---|---|---|---|
| FixedThreadPool | 构造时传入 | 与核心线程数相同 | LinkedBlockingQueue | 线程数量固定,只有核心线程并且不会被回收,没有超时机制 |
| CachedThreadPool | 0 | Integer.MAX_VALUE | SynchronousQueue | 线程数量不固定的线程池,只有非核心的线程,当线程都处于活动状态时,直接创建新线程来处理新任务,否则就利用空闲的线程。处于空闲状态超过60s的线程被回收 |
| ScheduledThreadPool | 构造时传入 | Integer.MAX_VALUE | DelayedWorkQueue | 非核心线程在闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务 |
| SingleThreadExecutor | 1 | 1 | LinkedBlockingQueue | 只有一个核心线程,确保所有任务在同一线程中按顺序执行 |
分析创建这四个线程池的方法的源码,最后都来到了ThreadPoolExecutor类的ThreadPoolExecutor构造方法,由此可见ThreadPoolExecutor才是真正的线程池。Executors作为线程池工厂,提供的四种线程池是利用不同参数创建的适应不同使用场景的线程池。
//ThreadPoolExecutor.java
/**
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数
* @param keepAliveTime 非核心线程闲置的超时时长
* @param unit 用于指定 keepAliveTime 参数的时间单位
* @param 任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中
* @param threadFactory 线程工厂,用于提供新线程
* @param handler 任务队列已满或者是无法成功执行任务时调用
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//···
}
线程池的简单使用
以手动创建一个核心数为5,最大线程数为7,空闲超时为20s,阻塞队列为数组实现的有界队列的ThreadPoolExecutor为例子:
ExecutorService executor = new ThreadPoolExecutor(
5, 7, 20L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(8)
);
for(int i = 0; i < 9; i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.valueOf(index)+ " " +Thread.currentThread().getName());
}
});
}
手动创建线程池的好处

阿里巴巴Java开发手册中使用强制标注:需通过手动创建 ThreadPoolExecutor 取代使用 Executors 提供的工厂方法。数据量并发量很大或难以把握时,应避免直接使用 Executors 提供的线程池,防止资源被耗尽
以CachedThreadPool为例子,CachedThreadPool将空闲线程销毁前的等待时间设置成了60s,同时阻塞队列类型是SynchronousQueue,不存储元素的队列。 CachedThreadPool 在一定程度上能够应对不断突增的并发任务,但是一旦任务量远远大于处理量,会造成线程数量的激增和资源的消耗,容易引发OOM。
手动创建线程池可以更好规范该线程池的职责,更好地管理这个线程池,让线程池在合适的场景下,可以用来处理适当的任务,而不是一颗随时会被引爆的炸弹。
总结
线程池,基于池化思想,体现了享元模式,可以用来管理线程并方便地并行执行任务的工具。本质上是对任务和线程解耦后进行管理,利用不同的构造参数可以构造出适合不同场景的线程池。优点是降低资源消耗,提高响应速度,提高线程的可管理性,可拓展性良好。缺点是参数不易配置,出错后易造成OOM。
篇幅问题,对线程池的设计和管理机制的分析安排在下一篇文章~
参考资料
- [1] JDK 1.8源码
- [2] Java线程池实现原理及其在美团业务中的实践
- [3] Google Developer Doucumentation - ThreadPoolExecutor
Java线程池的了解使用—筑基篇的更多相关文章
- Java线程池详解(二)
一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...
- 深入浅出Java线程池:源码篇
前言 在上一篇文章深入浅出Java线程池:理论篇中,已经介绍了什么是线程池以及基本的使用.(本来写作的思路是使用篇,但经网友建议后,感觉改为理论篇会更加合适).本文则深入线程池的源码,主要是介绍Thr ...
- Java线程池详解,看这篇就够了!
构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段. 基础知识 Execu ...
- java 线程池第一篇 之 ThreadPoolExcutor
一:什么是线程池? java 线程池是将大量的线程集中管理的类,包括对线程的创建,资源的管理,线程生命周期的管理.当系统中存在大量的异步任务的时候就考虑使用java线程池管理所有的线程.减少系统资源的 ...
- Java 线程池框架核心代码分析--转
原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...
- Java线程池的那些事
熟悉java多线程的朋友一定十分了解java的线程池,jdk中的核心实现类为java.util.concurrent.ThreadPoolExecutor.大家可能了解到它的原理,甚至看过它的源码:但 ...
- Java线程池的原理及几类线程池的介绍
刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...
- Java线程池与java.util.concurrent
Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...
- Java 线程池框架核心代码分析
前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和资源消耗都是很高的.线程池应运而生,成为我们管理线程的利器.Java 通过Executor接口,提供了一种标准的方法将任务的提交过 ...
随机推荐
- Jmeter(十五) - 从入门到精通 - JMeter导入自定义的Jar包(详解教程)
1.简介 原计划这一篇是介绍前置处理器的基础知识的,结果由于许多小伙伴或者童鞋们在微信和博客园的短消息中留言问如何引入自己定义的Jar包呢???我一一回复告诉他们和引入插件的Jar包一样的道理,一通百 ...
- 基于图嵌入的高斯混合变分自编码器的深度聚类(Deep Clustering by Gaussian Mixture Variational Autoencoders with Graph Embedding, DGG)
基于图嵌入的高斯混合变分自编码器的深度聚类 Deep Clustering by Gaussian Mixture Variational Autoencoders with Graph Embedd ...
- django开发自动化测试平台简介
Django的优点 1:功能完善.要素齐全:自带大量常用工具和框架(比如分页,auth,权限管理), 适合快速开发企业级网站. 2:完善的文档:经过十多年的发展和完善,Django有广泛的实践案例和完 ...
- python入门006
一:可变与不可变类型 可变类型:值改变,id不变,证明改的是原值,证明原值是可以被改变的 不可变类型:值改变,id也变了,证明是产生新的值,压根没有改变原值,证明原值是不可以被修改的 2.验证 2.1 ...
- java学习第二天 20207/7
一. 1.对传参进行了了解 2. 2.java的变量命名与c/c++有些不同在java中有¥,字母,下划线和数字,同样不可以是数字开头. java的布尔型为boolean 各个数据类型的信息: 注意: ...
- 简单的SQL语句学习
CREATE DATABASE db_test; USE db_test; CREATE TABLE USER( uid INT PRIMARY KEY AUTO_INCREMENT, usernam ...
- mybitis下choose..when. otherwise条件不起作用
我的代码如下: <select id="findList" resultType="TyArticle"> SELECT <include r ...
- SpringMVC如何从默认的index.jsp页面跳转到其他页面
最近学习SpringMVC时,想要做一个登录页面Login.jsp,发现Tomcat服务器默认进入的页面是WEB-INF/index.jsp,查询资料发现如果修改默认页面,还需要修改Tomcat文件目 ...
- day17 生成器, 面向过程, 三元表达式, 生成式
1. 生成器 生成器:就是一种自定义的迭代器,是用来返回多次值自定义迭代器的好处:节省内存 return只能返回一次值,函数就立即结束了yield 1.可以挂起函数,保存函数的运行状态 2.可以用来返 ...
- 【Nginx】如何获取客户端真实IP、域名、协议、端口?看这一篇就够了!
写在前面 Nginx最为最受欢迎的反向代理和负载均衡服务器,被广泛的应用于互联网项目中.这不仅仅是因为Nginx本身比较轻量,更多的是得益于Nginx的高性能特性,以及支持插件化开发,为此,很多开发者 ...