线程系列目录
- Thread线程从零认识到深层理解——初识
- Thread线程从零认识到深层理解——六大状态
- Thread线程从零认识到深层理解——wait()与notify()
- Thread线程从零认识到深层理解——线程安全
- 线程池从零认识到深层理解——初识
- 线程池从零认识到深层理解——进阶
博客创建时间:2020.09.25
博客更新时间:2021.02.25
注意:本系列博文源码分析取自于Android SDK=30,与网络上的一些源码可能不一样,可能他们分析的源码更旧,无需大惊小怪。
前言
在Android开发中常在非UI线程中处理耗时任务,此时需要使用线程来处理异步任务,如果每次都创建一个线程并在执行完毕后销毁,那么在需要多线程使用频繁的场景会消耗大量的资源和性能的损耗。且线程容易各自为政,很难控制。
为了解决线程的使用弊端,在Java1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给Runnable或者Callable,而Executor框架用来处理任务,Executor框架的核心实现类就是ThreadPoolExecutor
一、线程池意义
程序中并发的线程数量有很多,如果每次手动创建只是执行了时间很短的任务就结束了,那么频繁的创建线程和销毁线程会降低系统的效率,并降低CPU的使用效率。为了节省资源,提高效率,Java中提供了线程池来解决问题。
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池结构关系图:
二、线程池优点
- 降低系统资源消耗,通过多次重用已存在的线程,降低线程创建和销毁带来的性能消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 提高线程的可管控性,根据系统得承载能力,调整工作线程的数目,防止内存占用过多出现OOM(每个线程消耗约1MB),同时线程数太多,频繁的CPU切换会产生高额的调度时间成本和降低CPU调度片段时间。并且可以使用线程池对线程进行统一分配、调优和监控。
- 提供更强大的功能,延时定时线程池,指定线程的优先级。
- 避免大量的线程开启后因互相抢占系统资源导致阻塞现象
线程池是可以手动调用关闭的,根据不同的关闭策略,能将待执行或者正在执行的任务终止。
三、线程池创建
线程数量控制
并发线程数量应该在合理范围内,因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能执行的阈值,CPU就需要花费时间来判断线程的优先级。每开一个新的线程,都会耗费至少64K+的内存。
Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口java.util.concurrent.ExecutorService。
要配置一个线程池是比较复杂的,如果对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象,Executors是Executor,ExecutorService, ScheduledExecutorService,ThreadFactory,Callable的工具和工厂类。
如果是创建ThreadPoolExecutor,Executors调用newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool等方法创建线程池均是ThreadPoolExecutor类型。无论调用Executors类的何种创建方法,最终都会调用ThreadPoolExecutor类的构造函数。
代码如下:
/**
*
* @param corePoolSize 保留在池中的线程数,即使*处于空闲状态,
* 除非设置了{@code allowCoreThreadTimeOut}
* @param maximumPoolSize 池中允许的最大线程数
* @param keepAliveTime 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间。
* @param unit {@code keepAliveTime}参数的时间单位
* @param workQueue 在执行任务之前用于保留任务的队列。该队列将仅保存由{@code execute}方法提交的{@code Runnable} *任务。
* @param threadFactory 执行程序创建新线程时要使用的工厂
* @param handler 当执行被阻塞时要使用的处理程序,因为达到了线程界限和队列容量,注意它并不是一个我们常见的Handler
* @throws IllegalArgumentException 如果满足下列条件之一,则报IllegalArgumentException异常:
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException 如果 workQueue、threadFactory、handler之一为空,则报NullPointerException异常
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
四、构造函数详解
参数 | 说明 |
---|---|
corePoolSize | 核心线程数。当线程数小于该值时,线程池会优先创建新线程来执行新任务 |
maximumPoolSize | 线程池所能维护的最大线程数 |
keepAliveTime | 空闲线程的存活时间 |
unit | 参数keepAliveTime空闲线程的存活时间的单位 这是一个枚举类 |
workQueue | 任务队列,用于缓存未执行的任务 |
threadFactory | 线程工厂。可通过工厂为新建的线程设置更有意义的名字 |
handler | 拒绝策略。当线程池和任务队列均处于饱和状态时,使用拒绝策略处理新任务。默认是 AbortPolicy,即直接抛出异常 |
1. corePoolSize
默认情况下线程池是空的,只是任务提交时才会创建线程。如果当前运行的线程数大于corePoolSize,线程看下情况创建。如果调用线程池的prestartAllcoreThread方法,线程池会提前创建并启动所有的核心线程来等待任务。
2. maximumPoolSize
如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍然会创建新的线程来处理任务
3. keepAliveTime
当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间,超过这时间则回收。
<如果任务很多,并且每个任务的执行时间很短,则可以调大keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性来true时,keepAliveTime也会应用到核心线程上。
4. TimeUnit
keepAliveTime参数的时间单位。可选的单位有天Days、小时HOURS、分钟MINUTES、秒SECONDS、毫秒MILLISECONDS等。
5. workQueue
在执行任务之前用于保留任务的队列。该队列将仅保存由 execute 方法提交的 Runnable任务。该任务队列是BlockingQueue类型的,即阻塞队列。
6. ThreadFactory
可以使用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数,默认使用DefaultThreadFactory。
7. RejectedExecutionHandler
拒绝策略。当执行被阻塞时要使用的处理程序,因为达到了线程界限和队列容量,注意它并不是一个我们常见的Handler,默认是AbordPolicy。它的几种策略方式在下一篇博客中会详细说明。
ThreadPoolExecutor的构造参数需要满足一定的条件:
- <如果出现如下其中一种情况,则构建中抛IllegalArgumentException异常
1. corePoolSize < 0
2. keepAliveTime < 0
3. maximumPoolSize <= 0
4. maximumPoolSize < corePoolSize - <如果出现如下其中一种情况,则构建过程中抛NullPointerException异常
1. workQueue is null
2. threadFactory is null
3. handler is null
五、常见几种线程池
通过直接或间接的配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor。Executors提供了多种创建线程池的方法,其中有5种线程池比较常用:newFixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledTheadPool。
1. newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
从构造方法中可以看出corePoolSize=maximumPoolSize=nThreads,FixedThreadPool是可重用固定核心线程数的线程池,控制线程最大并发数
其特点是:
- 只有核心线程,并且数量是固定的,没有非核心线程
- keepAliveTime=0L,但是由于无非核心线程,该参数无效
- 任务队列默认采用无界的阻塞队列LinkedBlockingQueue,无大小限制
- 线程工厂可以自行定义,默认使用DefaultThreadFactory
2. CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
从构造方法中可以看出CachedThreadPool是一个根据需要自动创建线程的线程池。它有如下几个特点:
- corePoolSize=0,无核心线程
- maximumPoolSize=Integer.MAX_VALUE,非核心线程数原则上可以有20多亿个。
- KeepAliveTime=60s,线程空闲60s后将被回收,空闲线程灵活回收
- 任务队列使用的是SynchronousQueue,和LinkedBlockingQueue不同,它是一个不存储元素的阻塞队列。后面章节会对它详细描述。
根据其特点知道CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务,且CachedThreadPool使用SynchronousQueue队列,线程任务即提交即执行,无需等待
3. newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
从构建方法中可以看出newSingleThreadExecutor是使用单个工作线程的线程池,其corePoolSize和maximumPoolSize都为1。它的特点是:
- 只有一个核心线程,无非核心线程,任务按照指定顺序在一个线程中执行
- 其他参数和FixedThreadPool的一样,可以把它看作是Executors.newSingleThreadExecutor(1)的线程池。
newSingleThreadExecutor线程池一般用来进行数据库操作、文件操作或者打印操作等应用场景,但不适合可能引起IO阻塞及影响UI线程响应等操作
4. ScheduledTheadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
ScheduledThreadPoolExecutor是继承自ThreadPoolExecutor,实现了ScheduledExecutorService接口,它是一种特殊的线程池专用于实现定和周期性任务的线程池。根据其构造参数能看出如下特点:
- 核心线程自定义控制
- 非核心线程数可以达到Integer.MAX_VALUE
- 非核心线程的空闲保活时长是10ms
- 任务队列使用的是DelayedWorkQueue,是一种无界队列
5. SingleThreadScheduledExecutor
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
等价于 newScheduledThreadPool(1)
六、线程池使用流程
线程池的使用流程很简单,分三步走,创建线程池、提交任务、关闭线程池。
1. 创建线程池
创建时,通过配置线程池的参数,从而实现自己所需的线程池
Executor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory
);
2. 提交任务
execute和submit两种提交线程的方式,其详细分析请阅读下一篇博文
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行任务
}
});
threadPool.submit(new Runnable() {
@Override
public void run() {
... // 线程执行任务
}
});
3. 关闭线程池
shutdown和shutdownNow两种关闭线程池的方法,其具体区别和源码分析请阅读下一篇博文。
threadPool.shutdown();
threadPool.shutdownNow();
注意:
在开发过程中线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
这里进行讲解是为了帮助大家理解各种线程池的特性和构造,真实场景中老老实实用new ThreadPoolExecutor(…)的方式来创建线程池。因为使用Executors提供的方法返回的ThreadPoolExecutor有如下弊端:
- FixedThreadPool 和SingleThreadPool :使用的是LinkedBlockingQueue无界队列,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
- CachedThreadPool 和ScheduledThreadPool :允许的创建线程数量为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
总结
- newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
- newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
四种线程可以多多配合使用,其中Okhttp中线程池的实现和有特色,方便且安全,还避免上面两点的尴尬。
本篇博客主要从线程池的意义,优势,如何配参构建等方面讲解,该篇博文学习完成后你就已基本能掌握线程池的使用。
但是对于线程池的进阶深度学习,请继续阅读我的下一篇博客 《线程池从零认识到深层理解——进阶》中将进行详细介绍。
相关链接:
- Thread线程从零认识到深层理解——初识
- Thread线程从零认识到深层理解——六大状态
- Thread线程从零认识到深层理解——wait()与notify()
- Thread线程从零认识到深层理解——线程安全
- 线程池从零认识到深层理解——初识
- 线程池从零认识到深层理解——进阶
扩展链接:
博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !