目录
- 前言
- 一、Java 线程池的出现
- 二、Java 线程池的基础接口
- Executor 与 ExecutorService
- 三、Java 线程池的基础实现
- AbstractExecutorService
- ThreadPoolExecutor
- 1. 线程池的状态 —— 位运算
- 2. 生产消费模型 —— BlockingQueue(阻塞队列)
- 3. 任务执行者 —— Worker
- Worker 是什么
- Executor 如何管理 Worker
- 4. 执行函数 —— execute
- 5. 饱和策略 —— handler
- 四、为什么要使用线程池
前言
在上篇整理了 Java 线程与任务的概念,这篇说一说 Java 的线程池,在我们更加了解线程池的同时,也增加一些设计思路。
一、Java 线程池的出现
如果现在我们需要对并发的场景做性能优化,我们该从哪方面入手呢?显而易见,最简单的一个角度:节省线程创建与销毁的开销。
无论是IO密集应用还是计算密集应用,线程的管理都是需要成本的,所以从计算机的角度出发,我们还应该 限制线程的数量。创建线程的目的是使应用拥有更好的性能,而如果悬挂的线程过多,线程管理成本过高,应用的性能同样会下降。
对于以上两点需求,线程池就诞生了。
首先给大家看一下本文接口与实现类的关系:

二、Java 线程池的基础接口
Executor 与 ExecutorService
这是两个线程池的基础接口,ExecutorService 继承了 Executor。一般情况下,运用 ExecutorService 的实现比较多,因为拥有更完整的规范。先看一下这两个接口:
与 Runnable 接口类似,定义非常简单,将任务实现放入执行器,执行(execute)任务。
讯享网
在这里,我们可以看到线程池的雏形,为了更好的管理线程,实现线程池所需要的基本功能。
虽然 ExecutorService 的规范比 Executor 多了一些,但是总结无非就是三类:
- 关闭等控制类函数(池级别)
- 运行类函数(线程级别)
- 批量运行类型函数(第二种的扩展)
这里我们可以留意一下这两个接口:
感觉是不是很熟悉,这里先按下不表,下一节就能知道这里为什么要这样设计。
三、Java 线程池的基础实现
AbstractExecutorService
这个抽象类阶段性的实现了 ExecutorService 接口,在这个类中,明确了一些实现线程池的思路与步骤。
首先来看一下任务的基础实现与单任务的执行:
- 线程池的基础任务类型为 FutureTask
- 提交任务函数需要有2个步骤:新建任务对象 -> 执行器执行任务。
讯享网
至此,我们理解了 submit 函数看起来为什么那么眼熟,是因为秉承着和 FutureTask 一样的逻辑,所以也提供了两个整齐的接口。
下面我们来看一下批量运行的处理方法,invokeAny 涉及到线程管理的调度,这里先按下不表,我们先看 invokeAll 中的实现:
可以看到,取值是对整个 list 的一个遍历处理,因为是按 list 顺序 get 结果,所以其完成时间由 最慢任务决定。
到此为止,我们已经对线程池有了初步的认知,下面来看一下 Java 是怎么实现一个完整线程池的。

ThreadPoolExecutor
首先,我们可以通过构造方法来看一看,线程池的初始化最少需要什么元素。
参数说明:
- corePoolSize:线程池核心线程数。加入一个任务时,如果当前线程数(空闲+非空闲)小于 corePoolSize,即便当前有空闲的线程,也会创建新的线程来执行任务。如果当前线程数(空闲+非空闲)等于 corePoolSize,则不再重新创建线程。
- maximumPoolSize:线程池最大线程数。如果阻塞队列已满,并且当前线程数(空闲+非空闲)小于等于 maximumPoolSize ,就会创建新的线程来执行任务。
- keepAliveTime:如果当前线程数(空闲+非空闲)大于 corePoolSize ,并且空闲时间大于 keepAliveTime的话,就会将这些java技术基础多线程空闲线程销毁,释放资源。
- unit:时间的单位。
- workQueue:阻塞队列的实例。
- ThreadFactory : 线程工厂类,一般使用默认工厂。
- RejectedExecutionHandler:饱和策略。在任务过多时,对任务的处理策略。
1. 线程池的状态 —— 位运算
为了提升运行的效率,减少开销,我们在记录一些属性值的时候,会用到 bit 位的操作,即位运算。(比如算法课上要用哈夫曼树实现压缩,就需要 bit 位记录源文件的各种属性数据)。
如果不清楚位运算等概念的同学可以移步:
java中的 位运算 和 移位运算详解原码、反码、补码知识详细讲解
我们来看一下,ThreadPollExecutor 是如何运用一个 int 来记录两项属性的:
2. 生产消费模型 —— BlockingQueue(阻塞队列)
在 ThreadPoolExecutor 中,管理保存任务的数据结构就是阻塞队列。其中,生产的方式用的是 offer ,消费的方式是 poll 与 take。
接口规范中,很容易看到 poll 与 take 的区别。看一下在线程池中的应用:
Tips:
依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
如果是 poll 消费方式,则元素具备超时属性(具体可以看BlockingQueue的源码),会在取值处阻塞,直到达到超时(空闲)时间,返回null,在 runWorker() 函数中,调用销毁线程方法 processWorkerExit ,从而达到:线程数 > corePoolSize 时,空闲线程按时回收。
而 take 消费方式,则会一直阻塞等待,对于 getTask() 而言,总有返回值,即不会销毁线程。从而达到:线程数 <= corePoolSize 时,空闲线程不回收。
3. 任务执行者 —— Worker
在 ThreadPoolExecutor 中,有一个 Worker 的内部类。简单来说就是任务(runnable的实现 —— task)的容器,可以粗暴的理解为一个线程,但是不完全相同。
Worker 是什么
实际上 Worker 是一个任务容器,并且构造方法非常简单:
创建一个新的线程对象,并向其中添加一个任务。
Executor 如何管理 Worker
首先 Executor 是通过一个 set 来存储 Worker 的,确保每个 worker 的唯一性。
其次,对于线程池而言,最重要的就是 addWorker 和 runWorker 了。这里就不贴源码了,简单说一下这两个函数的区别与用途。
addWorker() 函数是 真正创建 Worker 实例并开启线程的地方 ,因为其中找到了 thread.start() 函数。稍微检索一下,我们就可以看到 execute() 函数里面调用了 addWorker,所以可以得知:线程池如果要执行任务,就需要调用execute() 函数。
runWorker() 函数实际上就是 Worker 的 run,(补充说明)。
4. 执行函数 —— execute
这里的代码,揭示了线程池线程池在线程数不同情况下的处理策略。
5. 饱和策略 —— handler
饱和策略是用于处理在线程异常与线程数已满的情况下,处理当前任务的处理方式。大家可以稍作了解。
- AbortPolicy: 中止执行提交的任务,抛出 RejectedExecutionException 异常;这是线程池中默认的策略。
- CallerRunsPolicy:用调用者所在的线程来执行任务,即在调用者视角,此任务串行在调用者的主线程中。
- DiscardPolicy:不处理直接丢弃掉任务(执行空函数);
- DiscardOldestPolicy:丢弃阻塞队列中头部任务(即存放最久的任务),执行当前任务。
四、为什么要使用线程池
经过以上了解,我们可以总结出线程池的价值:
- 降低资源消耗。通过复用已存在的线程,降低创建和销毁线程的资源消耗;
- 提高线程的可管理性。线程是稀缺资源,不能无限增生,过多的线程也会给计算机造成负担。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/10480.html