从使用到原理,探究Java线程池
什么是线程池
当我们需要处理某个任务的时候,可以新创建一个线程,让线程去执行任务。线程池的字面意思就是存放线程的池子,当我们需要处理某个任务的时候,可以从线程池里取出一条线程去执行。
为什么需要线程池
首先我们要知道不用线程池,直接创建线程有什么弊端:
第一个是创建与销毁线程的开销,Java中的线程是映射到操作系统线程上的,频繁地创建和销毁线程会极大地损耗系统的性能。
线程会占用一定的内存空间,如果我们在同一时间内创建大量的线程执行任务,很有可能出现内存不足的情况。
为了解决这两个问题我们引入线程池的概念,通过复用线程避免重复创建销毁线程带来的开销,同时可以设置最大线程数,避免同时创建大量线程导致内存溢出。
线程池的使用
1.线程池的核心参数
想掌握线程池首先要理解线程池构造函数的参数:
| 参数名 | 类型 | 含义 |
|---|---|---|
| corePoolSize | int | 核心线程数 |
| maxPoolSize | int | 最大线程数 |
| keepAliveTime | long | 保持存活时间 |
| workQueue | BlockingQueue | 任务存储队列 |
| threadFactory | ThreadFactory | 当线程池需要新创建线程的时候,会通过ThreadFactory创建 |
| Handler | RejectedExecutionHandler | 当线程池无法接受你提交的任务时所采取的拒绝策略 |
逐个解释这些参数是很难理解的,这里我结合一张线程池处理的流程图进行讲解:

当我们往线程池里提交任务时,如果线程池内的线程数少于corePoolSize,则会直接创建新的线程处理任务;
如果线程池的线程数达到了corePoolSize,并且存储队列没满,则会把任务放到workQueue任务存储队列里;
如果存储队列也满了,但是线程数还没有达到maxPoolSize,这个时候就会继续创建线程执行任务。注意:这个时候线程池内的线程数已经超过了corePoolSize,超过corePoolSize的线程不会一直存活在线程池内,当他们闲下来时并超过keepAliveTime设定的时间后,就会被销毁。
如果线程数已经达到了maxPoolSize,这个时候如果再来任务,线程池就采取Handler所指定的拒绝策略拒绝任务。
2.几种常见的线程池分析
Java为我们提供了几种常用的线程池,通过Executors类可以轻易地获取它们。下面我们通过分析这几种常用线程池的参数,了解这些线程池之间的异同。
- newSingleThreadExecutor
从字面上也好理解,这是一个单线程的线程池,它的构造参数如下(创建的时候不需要传参,这里指的是下一层调用线程池构造函数时的传参):
corePoolSize:1
maximumPoolSize(maxPoolSize):1
keepAliveTime:0L
workQueue:LinkedBlockingQueue
其他参数为默认值
大家按照照着上面的流程图模拟提交任务走一遍,就知道为什么这是一个单线程的线程池了。
当初次任务提交的时候,会创建一个线程执行任务;当提交第二个任务的时候,由于corePoolSize值为1,所以任务会放到任务队列中。由于任务队列选择的是LinkedBlockingQueue,底层结构是链表,理论上可以存放几乎无穷多的任务(默认的大小是Integer.MAX_VALUE),所以永远不会触发任务队列已满的条件,也就永远不会继续增加线程,所以该线程池能保持一个单线程的工作状态。
如果这个唯一的线程因为异常结束了,线程池会创建一个新的线程补上。通过阻塞队列,这个线程池能够保证任务是按顺序执行的。
- newFixedThreadPool
这是一个固定线程数的线程池,它的构造参数如下:
corePoolSize:n
maximumPoolSize(maxPoolSize):n
keepAliveTime:0L
workQueue:LinkedBlockingQueue
其他参数为默认值
如果理解了 SingleThreadExecutor 是如何限制只有一条线程执行任务的话,那这里固定线程数的原理也是一样的,关键是限定 corePoolSize 和 maxPoolSize 的大小一样,并使用几乎无限容量LinkedBlockingQueue
- newCachedThreadPool
可缓存的线程池,我理解的缓存是关于线程的缓存,它的构造参数如下:
corePoolSize:0
maximumPoolSize(maxPoolSize):Integer.MAX_VALUE
keepAliveTime:60L
workQueue:SynchronousQueue
其他参数为默认值
由于corePoolSize为0,所以任务提交到该线程池后会直接到阻塞队列。又由于阻塞队列采用的是SynchronousQueue,这是一种不存储任务的队列,一旦获得任务它就会分发给任务处理线程,所以直接触发流程图中第三个判断框:如果当前线程数小于maxPoolSize就创建线程。由于maxPoolSize设置了一个很大的值,基本上可以无限地创建线程,具体的数量取绝于JVM所能创建的最大线程数。若线程空闲60秒没任务处理便会被线程池回收。
该线程池在处理大量异步短链接任务的时候有较好的性能,在空闲的时候池内是没有线程的,节省了系统的资源。
- newScheduledThreadPool
corePoolSize:自定义
maximumPoolSize(maxPoolSize):Integer.MAX_VALUE
keepAliveTime:0
workQueue:DelayedWorkQueue
其他参数为默认值
由于maxPoolSize设置为Integer.MAX_VALUE,该线程池可以无限创建线程,由于阻塞队列选择了DelayedWorkQueue,所以可以周期性地执行任务。
- newWorkStealingPool
这个是JDK1.8新加入的线程池,底层使用的是ForkJoinPool。如果使用默认参数创建的话,该线程池能够创建足够多的线程以达到和系统相匹配的并行处理能力。每个线程都有自己的工作队列,如果当前线程工作完了,它会到别的工作队列中“窃取”任务执行,充分地利用了CPU的多核能力。
阿里巴巴关于创建线程池的约规
下面这段话搬运自阿里巴巴Java开发手册,相信大家看完上面的参数解释以及各种线程池的异同后,就不难理解这段约规了:
(六)并发处理
4. 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。
说明:Executors返回线程池的弊端如下:
1) FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2) CacheThreadPool和ScheduledThreadPool:
允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。
3. 线程池的数量设置为多少比较合适?
这个问题是没有固定答案的,我们可以先通过业界权威给出的公式计算线程池的数量,然后通过压测进一步确认具体的数量。
业界给出的指导公式是:
若任务是CPU密集型任务(比如说加密,计算哈希等),线程数可以设置为CPU核心数的1-2倍左右。
若任务是耗时IO型任务(比如说读写数据库,文件,网络等),线程数的公式为:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均处理时间)
这两种不同设计都遵循着尽力压榨CPU性能的原则。
4. 线程池的五种状态
线程池的五种状态都写在了ThreadPoolExecutor类中了,它们分别是:
- RUNNING:接受新任务,并处理新任务
- SHUTDOWN:不接受新任务,但是会处理队列中的任务
- STOP:不接受新任务,不处理队列中的任务,中断正在处理的任务
- TIDYING:所有任务已经结束,workerCount为零,这时线程会转到TIDYING状态,并将运行terminated()钩子方法
- TERMINATED:terminated()运行完成
5. 线程池运行的原理
我们先回顾一下如何新创建一个线程处理任务,看懂了再看线程池的原理就简单了:
//首先把我们要放在线程里运行的代码在Runnable接口实现类的run方法中封装好
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("处理任务 + 1");
}
}
//然后创建一个线程,把该Runnable接口实现类作为构造参数传给线程
public class Basic {
public static void main(String[] args) {
Thread thread = new Thread(new MyTask());
thread.start();
}
}
//最后调用线程的start方法运行,实际上调用的是Runnable的run方法
在上面的代码中,实现了Runnable接口的实例传入到线程类中,成为了线程对象的一个成员变量,线程运行的时候会调用该实例的run方法。
可以看到如果新创建一个线程来执行任务,任务会和线程耦合在一起。而线程池的关键原理在于它添加了一个阻塞队列,把任务和线程解耦了
在线程池中,有一个worker的概念,这个概念解释起来有点困难,你可以直接理解为worker就是一个线程工人,它手上拿着任务,当调用线程池的runWorker()方法时,线程就会处理一个任务,详细见下面代码
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {//会到阻塞队列中获取任务
w.lock();
//...
try {
//执行任务
} finally {
//...
w.unlock();
}
}
//...
} finally {
//...
}
}
从代码中可以看到线程池的关键代码就是一个while循环,在while循环中会不断地向阻塞队列中获取任务,获取到了任务就执行。
参考:
- 慕课网《玩转Java并发工具,精通JUC,成为并发多面手》课程
- https://www.oschina.net/question/565065_86540
- https://www.cnblogs.com/dolphin0520/p/3932921.html
- https://www.cnblogs.com/ok-wolf/p/7761755.html
从使用到原理,探究Java线程池的更多相关文章
- 从使用到原理学习Java线程池
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
- 【转载】从使用到原理学习Java线程池
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
- Java线程池原理解读
引言 引用自<阿里巴巴JAVA开发手册> [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销 ...
- Java线程池的原理及几类线程池的介绍
刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...
- Java线程池使用和分析(二) - execute()原理
相关文章目录: Java线程池使用和分析(一) Java线程池使用和分析(二) - execute()原理 execute()是 java.util.concurrent.Executor接口中唯一的 ...
- Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理
相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...
- 这么说吧,java线程池的实现原理其实很简单
好处 : 线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配.调优和监控,有以下好处: 1.降低资源消耗: 2.提高响应速度: 3.提高线 ...
- Java 线程池原理分析
1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...
- java线程池原理
在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1.减少在创建和销毁线程上所花的时间以及系统资源的开销 ...
随机推荐
- [工具] Docker安装及portainer GUI
一.Docker Engine安装 1.安装流程 1)移除旧版本(如果有旧版本) yum remove docker \ docker-client \ docker-client-latest \ ...
- LeetCode 题解 | 面试题 10.01. 合并排序的数组
给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B. 编写一个方法,将 B 合并入 A 并排序. 初始化 A 和 B 的元素数量分别为 m 和 n. 示例: 输入: A = [ ...
- CentOS 7下Apache + PHP + MySQL环境(LAMP)的安装
Step 1:更换阿里云 yum 源 curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7 ...
- 【WPF学习】第五十四章 关键帧动画
到目前为止,看到的所有动画都使用线性插值从起点到终点.但如果需要创建具有多个分段的动画和不规则移动的动画.例如,可能希望创建一个动画,快速地将一个元素滑入到视图中,然后慢慢地将它移到正确位置.可通过创 ...
- 三年前端,面试思考(头条蚂蚁美团offer)
小鱼儿本人985本科,软件工程专业,前端.工作三年半,第一家创业公司,半年.第二家前端技术不错的公司,两年半.第三家,个人创业半年.可以看出,我是个很喜欢折腾的人,大学期间也做过很多项目,非常愿意参与 ...
- JZOJ 1492. 烤饼干
1492. 烤饼干 (Standard IO) Description NOIP烤饼干时两面都要烤,而且一次可以烤R(1<=R<=10)行C(1<=C<=10000)列个饼干, ...
- 04 namenode和datanode
namenode元数据管理 1.什么是元数据? hdfs的目录结构及每一个文件的块信息(块的id,块的副本数量,块的存放位置<datanode>) 2.元数据由谁负责管理? namenod ...
- HTTPS 笔记
随着互联网的迅速发展,网络安全问题日益凸显,现在 Chrome 浏览器已经开始阻止非 https 网站的访问了.对于 https 的流程一直不是十分清晰,借着还没有完全复工有时间,大概画了个图总结一下 ...
- PHP的json_encode和json_decode的区别
经常搞混的两个PHP函数: json_encode()是对变量进行json编码 json_encode()为要编码的值,且该函数只对utf8编码的数据有效 json_decode($json)对jso ...
- Python卸载
前言 自己瞎折腾下载Python3.8.2,把之前下载好的python3.7.3覆盖掉.在运行之前Python环境的程序多次未果后.找到原因,Python3.7.3的包不支持Python3.8.2.于 ...