妈妈再也不用担心你不会使用线程池了

你。 提交于 2020-12-04 13:54:02

为什么要用线程池

使用线程池管理线程有如下优点:

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池介绍

ThreadPoolExecutor

Java 为我们提供了 ThreadPoolExecutor 来创建一个线程池,其完整构造函数如下所示:

  • int corePoolSize(核心线程数):线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程;核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态);如果设置了 allowCoreThreadTimeOut 为 true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。

  • int maximumPoolSize(线程池能容纳的最大线程数量):线程总数 = 核心线程数 + 非核心线程数。

  • long keepAliveTime(非核心线程空闲存活时长):非核心线程空闲时长超过该时长将会被回收,主要应用在缓存线程池中,当设置了 allowCoreThreadTimeOut 为 true 时,对核心线程同样起作用。

  • TimeUnit unit(keepAliveTime 的单位):它是一个枚举类型,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)。

  • BlockingQueue workQueue(任务队列):当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务,常用的 workQueue 类型:

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现 线程数达到了 maximumPoolSize 而不能新建线程 的错误,使用这个类型队列的时候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即无限大。

  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效,因为总线程数永远不会超过 corePoolSize。

  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误。

  4. DelayQueue:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。

  • ThreadFactory threadFactory(线程工厂):用来创建线程池中的线程,通常用默认的即可。

  • RejectedExecutionHandler handler(拒绝策略):在线程池已经关闭的情况下和任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务,在上面两种情况下,只要满足其中一种时,在使用 execute() 来提交新的任务时将会拒绝,线程池提供了以下 4 种策略:

  1. AbortPolicy:默认策略,在拒绝任务时,会抛出RejectedExecutionException。

  2. CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。

  3. DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

  4. DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。

线程池执行策略

当一个任务要被添加进线程池时,有以下四种执行策略:

  1. 线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务。

  2. 线程数量达到了 corePoolsSize,则将任务移入队列等待。

  3. 队列已满,新建非核心线程执行任务。

  4. 队列已满,总线程数又达到了 maximumPoolSize,就会由 RejectedExecutionHandler 抛出异常。

其流程图如下所示:

常见的四类线程池

常见的四类线程池分别有 FixedThreadPool、SingleThreadExecutor、ScheduledThreadPool 和 CachedThreadPool,它们其实都是通过 ThreadPoolExecutor 创建的

合理地配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为 CPU 密集型任务、IO 密集型任务和混合型任务。

  • CPU 密集型任务:线程池中线程个数应尽量少,推荐配置为 (CPU 核心数 + 1);

  • IO 密集型任务:由于 IO 操作速度远低于 CPU 速度,那么在运行这类任务时,CPU 绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高 CPU 利用率,推荐配置为 (2 * CPU 核心数 + 1);

  • 混合型任务:可以拆分为 CPU 密集型任务和 IO 密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

线程池工具类封装及使用

为了提升开发效率及更好地使用和管理线程池,我已经为你们封装好了线程工具类----ThreadUtils,依赖 AndroidUtilCode 1.16.1 版本即可使用,其 API 如下所示:

如果你使用 RxJava 很 6,而且项目中已经使用了 RxJava,那么你可以继续使用 RxJava 来做线程切换的操作;如果你并不会 RxJava 或者是在开发 SDK,那么这个工具类再适合你不过了,它可以为你统一管理线程池的使用,不至于让你的项目中出现过多的线程池。

ThreadUtils 使用极为方便,看 API 即可明白相关意思,FixedPool、SinglePool、CachedPool 分别对应了上面介绍的 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 这三种,IoPool 是创建 (CPU_COUNT * 2 + 1) 个核心线程数,CpuPool 是建立 (CPU_COUNT + 1) 个核心线程数;而所有的 execute 都是线程池外围裹了一层 ScheduledThreadPool,这里和 RxJava 线程池的实现有所相似,可以更方便地提供延时任务和固定频率执行的任务,当然也可以更方便地取消任务的执行,下面让我们来简单地来介绍其使用,以从 assets 中拷贝 APK 到 SD 卡为例,其代码如下所示:

看起来还不是很优雅是吧,你可以把相关的 Task 都抽出来放到合适的包下,这样每个 Task 的指责一看便知,如上例子可以改装成如下所示:

是不是瞬间清爽了很多,如果执行成功的回调中涉及了 View 相关的操作,那么你需要在 destroy 中取消 task 的执行哦,否则会内存泄漏哦,继续你上面的例子为例,代码如下所示:

以上是以 SimpleTask 为例,Task 的话会多两个回调,onCancel() 和 onFail(Throwable t),它们和 onSuccess(T result) 都是互斥的,最终回调只会走它们其中之一,并且在 Android 端是发送到主线程中执行,如果是 Java 端的话那就还是会在相应的线程池中执行,这点也方便了我做单元测试。



作者:Blankj
链接:https://www.jianshu.com/p/f667f86fa4bb
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


本文分享自微信公众号 - 国产程序员(Monday_lida)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!