一、线程调度原理
在任意时刻,CPU只能执行一条指令,每个线程获取到CPU的使用权之后才可以执行指令也就是说在任意时刻,只有一个线程占用CPU 处于运行状态
多线程并发,实际上是指多个线程轮流获取CPU 的使用权然后分别执行各自的任务,在可运行区中,其实有多个线程处于就绪状态的线程在等待CPU
而JVM 的一项任务,就是要负责线程的调度线程的调度就是按照特定的机制为多个线程来分配CPU的使用权,
二、线程调度模型
- 分时调度模型:它的思想是让线程轮流获取CPU 的使用权,并且平均每个线程占用CPU 的时间片
- 抢占式调度模型:Java虚拟机采用的模型,它的思想是优先让可运行池中优先级高的线程来占用CPU ,如果运行池中的优先级都一样,那就随机选择一个。如果程序想要干预运行顺序,那就给每个线程设置一个优先级
三、Android线程调度
由两个因素来决定:
- nice值
在Process 中定义
值越小,优先级越高
默认值是:THREAD_PRIORITY_DEFAULT,0
最低是19,后台优先级是16
可以设置成负数,优先级更高
如果后台进程比较多,会影响前台进程的运行,所以还需要另一种机制来处理这种特殊的情况
- cgroup
借助Linux 的cgroup来执行更严格的前台和后台策略,后台线程会被隐式的移到后台group ,当其他组的线程处于工作状态,那后台group 的线程就会被限制,使用很小的几率来利用CPU ,这种分离的调度策略既允许了后台线程来执行一些任务,同时不会对用户的前台线程造成影响,保证前台线程能使用更多的CPU
哪些线程会被移到后台group?
- 优先级设置的比较低的线程
- 不在前台运行的应用程序的线程
注意点
- 线程过多会导致CPU 频繁切换,降低线程运行效率异步不能无限制的使用
- 正确认识到线程执行任务的重要性,来决定哪种优先级,一般优先级与线程的工作量成反比
- 线程的优先级具有继承性,比如在UI线程中直接创建一个子线程,它的优先级和UI 线程一样高
四、异步方式汇总
Thread
- 最简单,常见的异步方式
- 不易复用,频繁创建及销毁开销大
- 复杂场景不易使用,比如要执行一个定时任务thread方式不方便使用
HandlerThread
- 本质是一个Thread ,自带消息循环
- 内部串行执行任务
- 比较适合那些需要长时间运行,不断的从队列中获取任务的场景
IntentService
- 内部实现:继承自service 在内部创建HandlerThread ,继承了HandlerThread 的特性
- 相对于service 来说,它的执行是异步的,不会阻塞UI线程的执行
- 优先级较高,不会被系统kill
AsyncTask
- Android提供的异步工具类
- 内部实现是两个线程池,一个handler
- 无需开发者去处理线程切换的问题
- 需注意版本不一致的问题,实现方式不一致,适配Android版本要在14以上
线程池
- Java提供的线程池
- 易复用,减少频繁创建、销毁的时间
- 功能强大:定时,任务队列,并发控制等
RxJava
- 由强大的Scheduler 集合提供
- 不同的类型区分,IO,Comptution
- 如果项目中集成了RxJava 推荐使用RxJava的线程池
五、线程使用准则
- 严禁直接new Thread
- 提供基础线程池供各个业务线使用,避免各个业务线各自维护一套线程池,导致线程数过多
- 根据任务类型选择合适的异步方式
优先级低,长时间执行,使用HandlerThread
定时执行,使用线程池
- 创建线程必须命名
方便定位线程归属
运行期,通过Thread.currentThread().setName()修改名字
- 对关键异步任务进行监控
异步不等于不耗时
AOP的方式来做监控
- 重视优先级设置,Java的线程调度是一个抢占式的调度模型
Process.setThreadPriority()
可以设置多次
六、锁定线程创建
- 项目变大以后收敛线程
- 项目源码,三方库,aar 中都有线程的创建
- 避免恶化的一种监控预防手段
解决方案
分析:
- 创建线程的位置去获取堆栈信息
- 所有的异步方式,都会走到new Thread
- 由于有的源码拿不到,不能直接在new Thread 的地方直接去获取堆栈信息,此时特别适合采用Hook手段,在特定的方法之内注入自己的逻辑
找Hook 点,构造函数或者特定方法
在这里,就可以找Thread 的构造函数在构造方法里加入自己的逻辑,获取调用栈信息拿到调用栈信息就能知道哪个调用不是调用我们自己的线程也可以知道调用栈信息是属于哪一个业务方
DexposedBridge.hookAllConstructors(Thread. class, new XC_MethodHook(
@Overrideprotected void afterHookedMethod (MethodHookParam param) throw Throwable {super.afterHookedMethod (param);Thread thread = (Thread) param.thisObject;LogUtils.i ( msg: thread. getName ()+" stack "+Log.getStackTraceString(new throwable);
七、线程收敛
常规方案:
- 根据线程创建堆栈考量合理性,使用统一线程库
- 各个业务线需要下掉自己的线程库使用统一的线程库
基础库怎么使用线程
- 直接依赖线程库
- 缺点:线程库更新可能会导致基础库更新
优雅的实现
- 基础库内部暴露API setEeecutor 基础库只需要修改一次
- 初始化的时候注入统一的线程库
统一线程库
- 区分任务类型:IO、CPU密集型
- IO密集型任务不消耗CPU,核心池可以很大
- CPU密集型任务:核心池大小和CPU核心数相关
八、模拟问题
1.线程使用为什么会遇到问题?
- 在项目的初期阶段,主要是关注业务功能,忽视了基础库的建设,具体到线程方面,没有采用统一的线程池,每个地方使用线程的方式比较乱,同时线程数量比较多
- 在项目发展壮大之后,遇到了一些线程的性能问题,比如说主线程卡顿,以及一些异步任务执行非常耗时
- Java的线程调度是一个抢占式的调度模型线程优先级比较重要,但是项目中并没有做这些区分,对IO和CPU密集型的任务也没有做区分,很有可能主线程抢不到时间片的情况
2.怎么在项目中对线程进行优化?
- 首先针对于项目中线程数过多的这种情况,做了线程收敛,通过hook 手段来获取每个线程运行的堆栈信息
- 然后结合业务场景来看这个线程是否需要单独来创建通过这种方式,尽可能在业务层面将线程收敛到统一的线程库当中,而对于基础库,每个基础库统一对外暴露一个接口,提供一个线程池实现的能力,在基础库使用之前来注入线程库,这样基础库都用到了线程库
- 基础线程库针对于IO密集型任务和CPU密集型任务做区分,对于IO密集型任务,比如网络请求,文件操作,它并不消耗CPU ,所以将核心池设置的比较大,而对于CPU密集型任务,如果核心数数量过高,他可能会导致CPU 频繁调度,反而会导致执行效率下降,因此根据CPU核心数来决定CPU线程池核心数大小
- 还做了其他处理,比如对重要的异步逻辑进行监控,监控他的执行时间,同时在执行异步任务的时候注重优先级以及线程名的设置