声明
本文是从前同事Blankj的开源项目安卓 offer 收割基拷贝过来仅作为自己的知识点
的收集。
线程池
优点
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行;
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
介绍
ThreadPoolExecutor
Java 为我们提供了 ThreadPoolExecutor 来创建一个线程池,其完整构造函数如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
int corePoolSize(核心线程数):线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程;核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态);如果设置了 allowCoreThreadTimeOut 为 true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。
int maximumPoolSize(线程池能容纳的最大线程数量):线程总数 = 核心线程数 + 非核心线程数。
long keepAliveTime(非核心线程空闲存活时长):非核心线程空闲时长超过该时长将会被回收,主要应用在缓存线程池中,当设置了 allowCoreThreadTimeOut 为 true 时,对核心线程同样起作用。
TimeUnit unit(keepAliveTime 的单位):它是一个枚举类型,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)。
-
BlockingQueue workQueue(任务队列):当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务,常用的 workQueue 类型:
SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现 线程数达到了 maximumPoolSize 而不能新建线程 的错误,使用这个类型队列的时候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即无限大。
LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效,因为总线程数永远不会超过 corePoolSize。
ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误。
DelayQueue:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
ThreadFactory threadFactory(线程工厂):用来创建线程池中的线程,通常用默认的即可。
-
RejectedExecutionHandler handler(拒绝策略):在线程池已经关闭的情况下和任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务,在上面两种情况下,只要满足其中一种时,在使用 execute() 来提交新的任务时将会拒绝,线程池提供了以下 4 种策略:
AbortPolicy:默认策略,在拒绝任务时,会抛出RejectedExecutionException。
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。
线程池执行策略
当一个任务要被添加进线程池时,有以下四种执行策略:
- 线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务。
- 线程数量达到了 corePoolsSize,则将任务移入队列等待。
- 队列已满,新建非核心线程执行任务。
- 队列已满,总线程数又达到了 maximumPoolSize,就会由 RejectedExecutionHandler 抛出异常。
其流程图如下所示:
常见的四类线程池
常见的四类线程池分别有 FixedThreadPool、SingleThreadExecutor、ScheduledThreadPool 和 CachedThreadPool,它们其实都是通过 ThreadPoolExecutor 创建的,其参数如下表所示:
参数 | FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool | CachedThreadPool |
---|---|---|---|---|
corePoolSize | nThreads | 1 | corePoolSize | 0 |
maximumPoolSize | nThreads | 1 | Integer.MAX_VALUE | Integer.MAX_VALUE |
keepAliveTime | 0 | 0 | 10 | 60 |
unit | MILLISECONDS | MILLISECONDS | MILLISECONDS | SECONDS |
workQueue | LinkedBlockingQueue | LinkedBlockingQueue | DelayedWorkQueue | SynchronousQueue |
threadFactory | defaultThreadFactory | defaultThreadFactory | defaultThreadFactory | defaultThreadFactory |
handler | defaultHandler | defaultHandler | defaultHandler | defaultHandler |
适用场景 | 已知并发压力的情况下,对线程数做限制 | 需要保证顺序执行的场景,并且只有一个线程在执行 | 需要多个后台线程执行周期任务的场景 | 处理执行时间比较短的任务 |
如果你不想自己写一个线程池,那么你可以从上面看看有没有符合你要求的(一般都够用了),如果有,那么很好你直接用就行了,如果没有,那你就老老实实自己去写一个吧。
合理地配置线程池
需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为 CPU 密集型任务、IO 密集型任务和混合型任务。
CPU 密集型任务:线程池中线程个数应尽量少,推荐配置为 (CPU 核心数 + 1);
IO 密集型任务:由于 IO 操作速度远低于 CPU 速度,那么在运行这类任务时,CPU 绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高 CPU 利用率,推荐配置为 (2 * CPU 核心数 + 1);
混合型任务:可以拆分为 CPU 密集型任务和 IO 密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。