狗哥肝了一下午的线程池,奉上~
线程池
欢迎来到狗哥多线程系列连载。本篇是线程相关的第六篇,前五篇分别是:
对线程还没有概念的小伙伴建议看看,哪里写错了,或写的不好的?给我提一些宝贵建议。感谢,今天进入线程的第六篇,线程池的学习。
什么是线程池?
线程池是一种池化技术,简单来说就是一个管理线程的池子。这个池子里面的线程数是固定的,当任务数量大于线程数量时,会对线程进行复用。一个线程执行完任务,就回到这等待下一个任务的招唤,也不要你销毁。类似的还有我们工作常接触的数据库连接池。java 中的线程池主要是 juc (java.util.concurrent)包来复制,主要是由 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 类来实现,后者在前者的基础上增加了定时执行的功能。
为什么使用线程池?
那为什么要使用线程池呢?手动创建不香吗?真的不香,手动创建的情景仅仅适合很少任务量的情况。比如:只有一个任务,这问题不大。
public class OneTask {
public static void main(String[] args) {
Thread thread0 = new Thread(new Task());
thread0.start();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
那如果我就是有 10000 个任务呢?要这样写吗?
public class OneTask {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
Thread thread0 = new Thread(new Task());
thread0.start();
}
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
运行结果:
Thread Name: Thread-9977
Thread Name: Thread-9975
Thread Name: Thread-9973
Thread Name: Thread-9951
Thread Name: Thread-9999
Excuse me? 创建 10000 个线程?疯了吧?java 是一门高级语言,很多底层的工作对我们来说都是黑盒,比如垃圾自动回收。每一个线程从创建到销毁都要占用资源,用完需要回收的。
10000 个线程造成的垃圾回收开销得有多大呀,如果还是需要耗费一定时间的任务呢?要是我的线程任务很简单就是打印个日志,使用线程的内存开销比任务执行本身的开销还要大,这时就会得不偿失。
简而言之,频繁创建线程带来两点很不友好的问题:
- 反复创建线程系统开销比较大,每个线程创建和销毁都需要时间。
- 过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。
但我的任务确实多,咋办?这个时候线程池就出现了,它的出现解决了以上两点问题。
首先,针对反复创建线程开销大的问题,线程池用固定数量的线程保持工作状态并复用。
其次,针对过多线程占用太多内存资源的问题,线程池根据需要创建线程,控制线程的总数量,避免占用过多内存资源。
java 的线程池
线程池嘛,就是个池子。这里面的线程是固定的且可控的,java 提供了 Executor 接口方便我们实现线程池,它的继承关系是这样的:
Executor.png
其中 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 是实现线程池的两个类,区别上文说过了。
另外,还有个 JDK1.7 才出现的线程池:ForkJoinPool,它适合执行可产生子任务的任务,第一步是拆分也就是 Fork,第二步是汇总也就是 Join。继承关系是这样的(后面再单独出一期专门研究这个线程池)。
ForkJoinPool.png
线程池的执行流程
图源:拉勾教育 - Java 并发编程.png
1、首先提交任务,检查核心线程池是否已满?满了丢进队列。未满则创建线程执行任务。2、队列是否已满?满了检查整个线程池是否已满?未满则是添加到队列中排队等待。3、整个线程池都没可用线程了,直接根据拒绝策略处理新任务。
线程池的参数
找到 ThreadPoolExecutor 的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue < Runnable > workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
它一共有如下 7 个参数:
图源:拉勾教育 - Java 并发编程
ThreadPoolExecutor 构造传入这 7 个参数,就可以创建一个线程池了,下面逐一解释:
1、corePoolSize 是核心线程数,就是指定线程池有多少始终活跃的线程,这个是根据业务需求定的,线程池执行过程的第一步就是检查核心线程数是不是都已经满了。
2、maximumPoolSize 是整个线程池的最大线程数,超出核心线程的部分有空闲,是可以进行回收的。所以正常情况下,线程池中的线程数量会处在 corePoolSize 与 maximumPoolSize 的闭区间内。
二者区别
maximumPoolSize 包含 corePoolSize 和 maximumPoolSize 减 corePoolSize。他两就像长工和临时工的区别。打个比方外包公司接到大项目,需要 100 个程序员才能搞定,可公司内部就只有 10 个猿。咋办?招 90 个临时的呗,干完活就开掉。原有的 10 个就是长工对应 corePoolSize ,即使没这项目他在公司还有活干。而剩下就 90 个就是临时的,对应 maximumPoolSize - corePoolSize,做完项目就不需要了。残酷吧?
所以,maximumPoolSize = corePoolSize + 临时工
3、keepAliveTime + 时间单位用于定义核心线程以外的线程(临时工,如果有的话)的存活时间,也就是说,这是定义临时工能活多久的参数。
4、ThreadFactory 是线程工厂,用于创建线程。有默认的,也可自定义实现。
5、workQueue 是阻塞队列,也就是暂时存任务的地方。
6、Handler 是拒绝策略,后面专门有一篇文章来探讨。
了解了这 7 个参数,现在我们设定 corePoolSize = 5,maximumPoolSize = 10,阻塞队列长度 = 100。再来看一个小视频,你对上面的流程图的理解会更深。
图源:拉勾教育 Java 并发编程
有哪 6 种线程池,如何使用?
除了自定义,良心的 java 给我们实现了 3 类,6 个线程池,分别是:
由 ThreadPoolExecutor 创建
- FixedThreadPool
- CachedThreadPool
- SingleThreadExecutor
由 ScheduledThreadPoolExecutor 创建
- ScheduledThreadPool
- SingleThreadScheduledExecutor
JDk 1.7 出现
- ForkJoinPool (原理较复杂,后面再讲)
FixedThreadPool(固定数目的线程池)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 固定线程数的线程池,核心线程数与最大线程数一样。
- 即使任务数 > 核心线程数,也不会再创建线程,而是扔到队列等待。
- 队列也满了,那就走拒绝策略。
- 线程闲置,马上回收。
线程数量固定,比较适用于耗时较长的任务。避免频繁回收和分配线程。
执行过程:线程池有 t0 ~ t9 十个线程,他们不断执行任务,期间任务不会减少不会增加,因为核心线程数 = 最大线程数。
FixedThreadPool 图源:拉勾教育 Java 并发编程
用法:用它生成 10 个线程,来执行 10000 个任务:
public class MyThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
executorService.execute(new Thread(new Task()));
}
executorService.shutdown();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
执行结果:可以看到来来去去都是 1~10 这几个线程在跑任务,并没有编号为 11 的线程。
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-6
Thread Name: pool-1-thread-8
Thread Name: pool-1-thread-7
Thread Name: pool-1-thread-7
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-6
Thread Name: pool-1-thread-10
Thread Name: pool-1-thread-3
CachedThreadPool(可缓存线程的线程池)
上源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 核心线程数 = 0,要是一直没任务线程数就 = 0。
- 最大线程数是无限增加的(最大可到 Integer.MAX_VALUE,为 2^31-1,基本不可能达到)。
- 线程数并非固定不变,默认闲置线程超过 60s 没工作,则销毁。
- 队列是 SynchronousQueue 容量是 0,不存储任务,只做中转。
适用于耗时较短的任务、任务处理速度 > 任务提交速度。就不会造成不断创建新线程。
用法:用它提交 10000 个任务,并执行。
public class MyThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.execute(new Thread(new Task()));
}
executorService.shutdown();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
执行结果:只要有任务提交就新建线程执行
Thread Name: pool-1-thread-826
Thread Name: pool-1-thread-827
Thread Name: pool-1-thread-233
Thread Name: pool-1-thread-303
Thread Name: pool-1-thread-321
Thread Name: pool-1-thread-833
Thread Name: pool-1-thread-825
Thread Name: pool-1-thread-832
Thread Name: pool-1-thread-69
Thread Name: pool-1-thread-18
Thread Name: pool-1-thread-830
Thread Name: pool-1-thread-829
SingleThreadExecutor(单线程的线程池)
源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 最大线程数和核心线程都 = 1,有且只有一个线程。
这货有啥使用场景?还真有,比如:用于所有任务都需要按被提交的顺序依次执行的场景
用法:
public class MyThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10000; i++) {
executorService.execute(new Thread(new Task()));
}
executorService.shutdown();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
结果:
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
···
ScheduledThreadPool(定时或周期的线程池)
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
适用场景:定时或周期性执行任务,它有三个重要的方法:
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
// 延迟指定时间后执行一次任务(这里是 10s 后执行完任务,结束)
service.schedule(new Task(), 10, TimeUnit.SECONDS);
// 以固定的频率执行任务(表示第一次延时后每次延时多长时间执行一次),第二个参数是第一次延迟的时间,第三个参数是周期
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
// 类似于第二个,区别在于周期的定义。第二个方法的周期是以任务开始时间为起始时间计时,而这个是以任务结束的时间为起始时间
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
用法:
public class MyThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 10000; i++) {
executorService.schedule(new Thread(new Task()), 10, TimeUnit.SECONDS);
}
executorService.shutdown();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
SingleThreadScheduledExecutor(定时或周期的单线程线程池)
源码:
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
SingleThreadScheduledExecutor 只有一个线程且支持定时、周期功能。很明显是 ScheduledThreadPool 和 SingleThreadExecutor 的结合体。适用于对执行顺序有要求,且需要定时或周期执行的任务
用法:
public class MyThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
for (int i = 0; i < 10000; i++) {
executorService.schedule(new Thread(new Task()), 10, TimeUnit.SECONDS);
}
executorService.shutdown();
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
总结
本文聊了聊线程池是什么?为什么?怎么用?以及分析了线程池的执行过程,各参数含义、Java 各线程池的使用以及使用场景。相信你看完会有所收获,当然,由于篇幅原因,阻塞队列、拒绝策略等参数后面再发文探讨。另外,狗哥非科班出身水平有限,如文章有错,请友善指正,感激不尽。
巨人的肩膀
- https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=239
-END-
- zeromq的安装,部署(号称最快的消息队列,消息中间件)
- OpenDaylight新建HelloWorld工程并集成版本
- 摸索出来的chrom调试前后台数据(Java&&Ajax)交互的方法分享一下咯!!!
- 机器学习:单词拼写纠正器python实现
- java.lang.Exception: 资源处理失败,失败原因:com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown
- Server Tomcat v7.0 Server at localhost failed to start.
- Ovs+Dpdk简单实践
- Spark入门,概述,部署,以及学习(Spark是一种快速、通用、可扩展的大数据分析引擎)
- 创建基于MailKit和MimeKit的.NET基础邮件服务
- 把一个矩阵行优先展成一个向量,numpy.ravel() vs numpy.flatten()区别
- dataframe插入数据报错SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a
- python 对矩阵进行复制操作 np.repeat 与 np.tile区别
- python标准异常:中英文对比
- 激活windows10转到电脑设置的水印消失3种方法总结
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- AkShare-中国宏观-航贸运价指数
- 您应该知道的11个JavaScript和TypeScript速记
- AkShare-中国宏观-央行货币当局资产负债
- AkShare-中国宏观-FR007利率互换曲线
- Github标星59.7k:用动画的形式呈现解LeetCode题目的思路
- 《爱情公寓》电影版,十年一瞬间(下)
- 实战:上亿数据如何秒查
- 《爱情公寓》电影版,十年一瞬间(上)
- Ajax爬取今日头条街拍美图
- 现在的房租有多高(杭州)?
- 英雄联盟皮肤大拼图
- Python爬虫-MongoDB
- Python爬虫-selenium
- 用Python识别图形验证码,实现自动登陆!
- 当Docker遇到Intellij IDEA,再次解放了生产力~