04.设计轻量级线程池库
目录介绍
- 01.整体概述说明
- 1.1 项目背景
- 1.2 遇到问题
- 1.3 基础概念介绍
- 1.4 设计目标
- 1.5 产生收益分析
- 02.线程池基础知识
- 2.1 为何有线程池
- 2.2 如何管理线程
- 2.3 线程池创建
- 2.4 线程池生命周期
- 2.5 线程池弊端分析
- 2.6 线程池方案对比
- 2.7 优雅创建线程池
- 03.线程分析
- 3.1 线程为何有优先级
- 3.2 线程池如何管理线程
- 3.3 线程之间如何切换
- 3.4 线程任务耗时统计
- 3.5 线程数据共享
- 3.6 多线程并发优化
- 3.7 如何查看程序线程数
- 3.8 将异步转为同步
- 04.线程池封装思路
- 4.1 封装总体思路
- 4.2 主和子线程通信
- 4.3 使用静态代理模式
- 4.4 处理不同线程池
- 4.5 阻塞队列的选择
- 4.6 设置自定义Task
- 4.7 线程池使用分析
- 05.方案基础设计
- 5.1 整体架构图
- 5.2 UML设计图
- 5.3 关键流程图
- 5.4 接口设计图
- 5.5 模块间依赖关系
- 06.其他设计说明
- 6.1 性能设计
- 6.2 稳定性设计
- 6.3 灰度设计
- 6.4 降级设计
- 6.5 异常设计
- 07.其他说明介绍
- 7.1 参考链接
01.整体概述
1.1 项目背景
1.2 遇到问题
- 一般开启线程的操作如下所示
new Thread(new Runnable() { @Override public void run() { //做一些任务 } }).start();
- 创建了一个线程并执行,它在任务结束后GC会自动回收该线程。在线程并发不多的程序中确实不错,而假如这个程序有很多地方需要开启大量线程来处理任务,那么如果还是用上述的方式去创建线程处理的话,那么将导致系统的性能表现的非常糟糕。
- 主要的弊端有这些,可能总结并不全面
- 大量的线程创建、执行和销毁是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM
- 大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿
- 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失
1.3 基础概念介绍
- 线程池如何降低系统资源消耗?通过重用已存在的线程,降低线程创建和销毁造成的消耗,其核心就是线程的复用机制
- 线程池提高系统响应速度?当有任务到达时,无需等待新线程的创建便能立即执行
- 线程池如何管控并发数?线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
- 线程池还有什么功能?线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
1.4 设计目标
- 打造一个轻量级线程池工具,支持多种使用场景。比如执行核心任务,执行cpu类型任务,执行io类型任务,可以随意切换。
- 简化handler处理消息,可以处理主线程或者子线程消息。同时支持使用HandlerThread处理大量消息逻辑,比如轮训操作。
1.5 产生收益分析
02.线程池基础知识
2.1 为何有线程池
- 线程池好处:
- 1)降低资源消耗;2)提高相应速度;3)提高线程的可管理性;4)可以提供定时定期可控线程数量等功能
- 线程池的实现原理:
- 当提交一个新任务到线程池时,判断核心线程池里的线程是否都在执行。如果不是,则创建一个新的线程执行任务。如果核心线程池的线程都在执行任务,则进入下个流程。
- 判断工作队列是否已满。如果未满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
- 判断线程池是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果满了,则交给饱和策略来处理这个任务。
2.2 如何管理线程
- 大概的流程图如下
image
- 文字描述如下
- ①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
- ②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
- ③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
- ④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
2.3 线程池创建
- 通过Executors的工厂方法获取这五种线程池,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,具体可以看看源码,这里省略呢……
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();
- 每种线程池大概的作用简单说一下
- newFixedThreadPool(使用阻塞队列:LinkedBlockingQueue) 建立固定线程数量的线程池,每提交一个任务就会建立一个线程,直到达到线程池的最大大小。若是其中某个线程发生异常的话, 那么会有一个新的线程来替代它
- newSingleThreadExecutor(使用阻塞队列:LinkedBlockingQueue) 建立单个线程的线程池,也就是只有一个线程,那么,全部提交上来的任务就是按顺序执行。若是这个线程发生异常的话,那么 会有一个新的线程来替代它
- newCachedThreadPool(使用阻塞队列:SynchronousQueue) 建立一个大小不受限制的,可缓存的,线程数量自动扩容,自动销毁的线程池,若是线程池的大小超过了处理任务所须要的线程,那么线程池会回收部分线程(时间是60s)
- newScheduledThreadPool (使用阻塞队列:DelayedWorkQueue) 建立一个大小无限的线程池,支持定时任务和周期性执行的任务
2.4 线程池生命周期
2.5 线程池弊端分析
- 线程池如果使用不当也可以能造成代码缺陷
- FixedThreadPool和SingleThreadPoolPool : 请求队列使用的是LinkedBlockingQueue,容许的请求队列的长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而致使 OOM
- CachedThreadPool和ScheduledThreadPool : 容许建立的线程数量是Integer.MAX_VALUE,可能会建立大量的线程池, 从而致使 OOM
- 以上也就是建议本身配置线程池的缘由,那么线程池的那些参数究竟该如何配置? 线程池大小的配置:通常根据任务类型 和 CPU的核数(N)工具
2.8 线程池方案对比
- 推荐ArchTaskExecutor;其次是RxJava;再其次是 AsyncTask
2.7 优雅创建线程池
- 1、可以创建CPU密集型和io密集型的线程池。CPU密集型的线程池和核心数相关,核心线程数可以是2到4之间,最大线程数可以是2倍的核心数再加一。
- 2、针对IO密集型的线程池,为例能够让越快执行,核心线程数可以为0,最大线程数可以是int的最大值。但是每个设备并不能创建这么多的线程,一旦线程数超限,仍然会发生崩溃。可以查看下设备的创建线程数的上限,/proc/sys/kernel/threads-max记录的设备能够创建线程的最大数,根据这个上限来设置最大的线程池。同时还可以创建一个备份的线程池,当线程用光后,启用备份的线程池,备份线程池可以只有一个或者多个线程。
03.线程分析
3.1 线程为何有优先级
3.2 线程池如何管理线程
3.3 线程之间如何切换
3.4 线程任务耗时统计
3.5 线程数据共享
3.6 多线程并发优化
3.7 如何查看程序线程数
3.8 将异步转为同步
- 场景说明
- 平时获取子线程返回结果以异步回调的方式获取返回到主线程,其实也可以通过某种方法转为同步返回。那么如何将这种异步回调改成同步式的回调?
- 应用场景:
- 1.单个线程处理结果返回到主线程;2.多个子线程并发请求,最终合并返回结果到主线程
- 将异步转为同步
- 第一种方式:使用sleep,直接在主线程睡眠,等待子线程返回。缺点:不明确子线程执行时间情况下,不一定能拿得到返回结果
- 第二种方式:使用join,阻塞调用此方法的线程进入 TIMED_WAITING 状态,直到线程完成。
- 第三种方式:使用Future,这是线程创建的又一种实现方式,只不过是阻塞异步的。
- 第四种方式:使用CountDownLatch,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
- 第五种方式:使用同步屏障CyclicBarrier,线程间同步阻塞是使用的是ReentrantLock,可重入锁。
- 第六种方式:使用信号量Semaphore,与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。
- 第七种方式:使用移相器Phaser,重量级并发类,jdk7被引入,用来解决控制多个线程分阶段共同完成任务的情景问题。其作用相比CountDownLatch和CyclicBarrier更加灵活。
- CountDownLatch和CyclicBarrier区别
- CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
- CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待,类似有福同享,有难同当
- 如何将callback转为同步
04.线程池封装思路
4.1 封装总体思路
4.2 主和子线程通信
4.3 使用静态代理模式
- 静态代理理解和应用:AbsTaskExecutor(抽象类) + DefaultTaskExecutor(委托类) + DelegateTaskExecutor(代理类)
- AbsTaskExecutor,定义TaskExecutor的功能,即在主线程/IO线程执行任务。
- DefaultTaskExecutor,ArchTaskExecutor的默认代理类,当client没有手动设置delegate时,使用其执行任务。
- 使用了newFixedThreadPool的线程池模型。 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。源码设定最大并发数是4。postToMainThread时,mMainHandler初始化使用了双重效验锁。
- DelegateTaskExecutor,实现了多个抽象方法,这里使用代理模式,实现后,执行全部转交给mDelegate,而此类也提供setDelegate的方法,允许client从外部提供一个TaskExecutor,这样设计的好处:
- 1.通过代理,实际的执行模型(如:DefaultTaskExecutor的ExecutorService)交给代理类实现,隐藏了实现细节。
- 2.给client比较高的自由度,客户端可以定义自己的TaskExecutor,从而定义实际的执行模型及队列等,也有利于方法埋点等操作。
- 另外,通过getMainThreadExecutor与getMainThreadExecutor静态方法将sMainThreadExecutor与sIOThreadExecutor暴露给外部,而sMainThreadExecutor与sIOThreadExecutor相当于一个适配器,将ArchTaskExecutor的任务执行转换成标准的Executor,并未暴露mDelegate中的ExecutorService。
4.4 处理不同线程池
- 处理io密集流
- 处理比较核心的任务
- 处理cpu密集任务
4.5 阻塞队列的选择
- 1.ArrayBlockingQueue: 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- 2.LinkedBlockingQueue: 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量一般要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和newSingleThreadExecutor使用了这个队列
- 3.SynchronousQueue: 一个不存储元素的阻塞队列。每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- 4.PriorityBlockingQueue: 一个具备优先级的无限阻塞队列。 五、DelayedWorkQueue 任务是按照目标执行时间进行排序。
4.6 设置自定义Task
- AbsTaskExecutor使用代理模式实现了一个内部的线程池 DefaultTaskExecutor 提供使用,同时支持切换主线程的功能。其中如果使用者没有设置自己的 TaskExecutor ,那么 AbsTaskExecutor 将会使用 DefaultTaskExecutor 作为默认实现的线程池。
- 后面如果需要用到线程池的地方,就不用再自己写一堆实现方法了,直接调用 AbsTaskExecutor 就行了,如果不满足要求,就自己再定义一个 TaskExecutor 。
4.7 线程池使用分析
其他
- Android平台下的cpu利用率优化实现
- https://juejin.cn/post/7253062314307223611#heading-4