编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

09.线程优化实践操作

目录介绍

  • 01.线程优化的背景
    • 1.1 实践背景说明
    • 1.2 遇到场景记录
    • 1.3 线程优化整体思路
    • 1.4 查找线程的来源
  • 02.线程现状和改造
    • 2.1 线程使用问题
    • 2.2 线程问题有哪些
    • 2.3 使用线程池管理线程
    • 2.4 常规解决方案
    • 2.5 线程改造方案
    • 2.6 线程改造的计划
  • 03.线程的一些基础
    • 3.1 主线程与子线程
    • 3.2 UI线程的特点
    • 3.3 如何理解多线程
    • 3.4 线程交互Handler
  • 04.异步任务的方式
    • 4.1 Thread直接创建
    • 4.2 Thread+Looper+handler
    • 4.3 AsyncTask
    • 4.4 HandlerThread
    • 4.5 IntentService
    • 4.6 线程池Executors
  • 05.如何检测线程
    • 5.1 线程检测方式
    • 5.2 Profiler检测线程
    • 5.3 ADB获取检测线程
    • 5.4 getAllStackTraces获取线程
    • 5.5 读取伪文件检测线程
  • 06.如何去统计线程
    • 6.1 创建线程的方式
    • 6.2 创建线程池的方式
    • 6.3 扫描线程和线程池
    • 6.4 扫描结果统计分析
  • 07.线程优化思路
    • 7.1 优化线程思路
    • 7.2 优化线程池思路
    • 7.3 行业优化方案思路
  • 08.高效的使用线程
    • 8.1 多线程安全问题
    • 8.2 频繁发送消息问题
    • 8.3 轻量级线程池
    • 8.4 异步线程管理
    • 8.5 线程绑定生命周期
    • 8.6 排查线程CPU使用率飙升
  • 09.优化线程的使用
    • 9.1 设置线程优先级方式
    • 9.2 避免Thread直接创建
  • 10.线程卡顿是怎么回事
    • 10.1 线程如何卡顿
    • 10.2 如何模拟线程卡顿
    • 10.3 线程卡顿监控耗时
    • 10.4 如何解决线程卡顿

01.线程优化的背景

1.1 实践背景说明

  • 说一下背景
    • 1.编译器自带的profiler工具显示项目在启动阶段有上百个线程被创建,这么多的线程真的都是由App创建的吗?
    • 2.过多的线程不仅仅带来了内存上的消耗同时也降低了cpu调度的效率,过多的cpu调度带来的消耗的坏处甚至超过了多线程带来的好处。
    • 3.每个设备能够创建的线程数是有限的,创建的线程数超限就会发生崩溃。为解决线程数超限带来的崩溃问题就需要降低线程的个数,既然需要降低线程的个数,那需要查找线程的来源。
  • 线程带来一定的压力,主要表现在以下几个方面:
    • 线程数量增多:一些库可能会在后台启动一些线程来执行任务,这样会增加系统中线程的数量,从而导致系统资源的浪费。
    • 线程竞争:一些库可能会在同一时间启动多个线程来执行任务,这样会导致线程之间的竞争,从而影响程序的执行效率。
    • 线程阻塞:一些库可能会在执行任务时阻塞主线程,从而导致程序的卡顿和响应速度变慢。

1.2 遇到场景记录

  • 通常会遇到以下几个问题
    • 某个场景会创造过多的线程,最终导致线程OOM
    • 线程池过多问题,比如三方库有一套线程池,自己项目也有一套线程池,随着三方/二方业务接入,导致了不相兼容的线程池数越多,降低了全体线程池数的调度效率,比如多个okhttp的调用
    • 老代码原因导致,new Thread横行,又或者是各种线程使用不规范,导致工程混乱
    • 即使是空闲时候,依旧有线程在不断Waiting
    • 偶发各种线程死锁问题
  • 导致的结果
    • 会遇到各种线程不明的情况,对排查问题或者解决问题带来极大的考验。

1.3 线程优化整体思路

  • 为了解决线程问题,用下面的思路来进行线程优化:
    • 线程检测,评估优化空间。
    • 线程统计,收集优化范围。
    • 线程和线程池优化,线程数收敛。
    • 线程栈裁剪,减少线程内存。

1.4 查找线程的来源

  • 如何查找线程的来源?
    • 新建一个空项目,创建完成后不添加任何代码直接运行项目,然后查看profiler中显示的线程,可以发现profiler中显示有10多个线程。
    • 通过线程名,大致可以推测出线程的创建者分别有安卓系统、profile自身、Android Studio等,除此之外还有主线程。
  • 主要是哪些线程呢?
    • 一个空项目,至少包含主线程main、Binder线程,FinalizerDaemon线程、FinalizerWatchdog线程、RenderThread、ReferenceQueued、HeapTaskDaemon、SignalCatcher、GCDaemon等等。

02.线程使用的现状

2.1 线程使用问题

  • 线程数过多,本质上是直接创建线程,没有复用线程,每当来一个任务就创建一个线程,使得创建的线程越来越多。
  • 项目中使用了过多的线程池,没有统一线程池,最好一个项目中只有一个线程池。

2.2 线程问题有哪些

  • UI 线程如果阻塞,会导致界面卡顿、ANR 等问题。
    • 为了保证 UI 的流畅性,一些耗时的工作就不能在主线程中进行处理了,例如,网络操作、I/O 操作等。绝大多数,耗时操作都应该在子线程中处理。

2.3 使用线程池管理线程

  • 因为在系统中创建线程是一个比较耗费资源的事, 所以不能频繁创建和释放线程, 因此在效率上考虑通常会使用线程池, 同时也便于线程的管理。
    • Android中的AsyncTask就使用了线程池。建议封装简单型的线程池库,主要是处理cpu,io,ui级相关的业务逻辑。

2.4 常规解决方案

  • 第一种:团队增加cr机制,review代码准入
    • 比如定制Thread的规范,又或者是定义项目统一的线程池,在项目中去使用。这个方案优点就是可操作性强,便于团队去实施。
  • 第二种:字节码插桩监控线程
    • 通用的方案实践,比较流行的方案是通过字节码插桩的方式,统一做线程监控亦或进行线程统一,比如监控处理的matrix,还有优化相关的booster等。

2.5 线程改造方案

  • 方案1:使用统一的线程池
    • 不要手动创建线程,将项目中使用线程的地方替换成线程池,最好一个项目中只有一个线程池。
  • 方案2:对于三方库创建的线程,如果三方库有替换线程的接口,则可以将三方库创建的线程替换成统一的线程池。
    • 如果三方库没有替换线程的接口,可以把三方库的源码下载下来,直接修改源码,然后将修改后的源码上传到公司的仓库。
  • 方案3:使用字节码方案统一线程
    • 可以使用字节码插桩的方式将项目中的线程替换成统一的线程池,这个一下子搞不定,而且成本会比较高,需要经过严格的线下测试。

2.6 线程改造的计划

  • 前期计划
    • 1、创建全局统一的线程池,将项目中使用线程的地方替换成线程池,如果三方库有替换线程的接口,则将三方库创建的线程替换成统一的线程池。
    • 2、最好进行灰度测试,通过服务端设置一个开关,开关默认开启,开关开了,则启用统一的线程池,否则使用老代码。
    • 3、由于三方库的线程并没有修改掉,项目中引入线程池,可能会导致线程数不降反升。
  • 长期计划
    • 1、使用字节码插桩的方式将项目中的线程替换成统一的线程池,这个影响范围也比较大,需要经过严格的测试,滴滴开源的booster已经提供了这种功能。
  • 长效机制
    • 1、为了防止后续在项目里面手动创建线程,可以在编译期间检测线程,如果手动创建线程,直接编译失败,缺点就是会导致编译时间变长。

03.线程的一些基础

3.1 Android中的线程

  • 主线程(有的也成UI线程)
    • 在Android当中, 当应用启动的时候,系统会给应用分配一个进程,顺便一提,大部分应用都是单进程的,不过也可以通过设置来使不同组件运行在不同的进程中。
    • 在创建进程的同时会创建一个线程,应用的大部分操作都会在这个线程中运行。所以称为主线程,同时所有的UI控件相关的操作也要求在这个线程中操作,所以也称为UI线程。
  • 为何会有子线程
    • 因为所有的UI控件的操作都在UI线程中执行,如果在UI线程中执行耗时操作,例如网络请求等,就会阻塞UI线程,导致系统报ANR(Application Not Response)错误。
    • 因此对于耗时操作需要创建工作线程来执行而不能直接在UI线程中执行。这样就需要在应用中使用多线程,但是Android提供的UI工具包并不是线程安全的,也就是说不能直接在工作线程中访问UI控件,否则会导致不能预测的问题, 因此需要额外的机制来进行线程交互,主要是让其他线程可以访问UI线程。

3.2 UI线程的特点

  • Android UI线程(主线程)有几个特点:
    • 只能在 UI 线程操作 UI 视图,不能在子线程中操作。不能在 UI 线程中进行耗时操作,否则会阻塞 UI 线程,引起 ANR、卡顿等问题。
  • 在 Android 开发中,我们通常将一些耗时的操作使用异步任务的方式进行处理。
    • 例如这样一种这种场景,子线程在后台执行一个异步任务,任务过程中,需要 UI 进程展示进度,这时我们就需要一个工具来实现这种需求。
    • Android 系统为开发人员提供了一个异步任务类(AsyncTask)来实现上面所说的功能,即它会在一个子线程中执行计算任务,同时通过主线程的消息循环来获得更新应用程序界面的机会。

3.3 如何理解多线程

  • 为什么会有多线程:
    • 因为并行执行多任务

3.4 线程交互Handler

  • 在Android当中, 工作线程主要通过Handler机制来访问UI线程。
    • 当然还有一些封装好的类例如AsyncTask可以使用, 但是本质仍是使用Handler。
  • Handler机制主要由4部分组成, Looper, 消息队列, 消息类和Handler组成。
    • 其中Looper和消息队列是和线程绑定的, 每个线程只会有一个Looper和一个消息队列, 当Looper启动时,它会无限循环尝试从消息队列中获取消息实例,如果没有消息则会阻塞等待。
    • 当Handler发送消息时会把消息实例放入消息队列中,Looper从中取得消息实例然后就会调用Handler的相关方法,因为Looper是线程绑定的, 如果绑定的是UI线程,那么此时Handler的方法就会在UI线程中得到执行,线程间就是这样进行交互的。

04.异步任务的方式

4.1 Thread直接创建

  • 最直接的方式,就是使用 Java 提供的 Thread 类进行线程创建,从而实现异步。
  • 遇到的问题有哪些?
    • 继承Thread,或者实现接口Runnable来开启一个子线程,无法准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。当线程出现异常的时候,如何避免导致崩溃问题? = 开启Thread线程案例如下
    • 一般开启线程的操作如下所示
      new Thread(new Runnable() {
          @Override
          public void run() {
              //做一些任务
          }
      }).start();
    • 分析
      • 创建了一个线程并执行,它在任务结束后GC会自动回收该线程。
      • 在线程并发不多的程序中确实不错,而假如这个程序有很多地方需要开启大量线程来处理任务,那么如果还是用上述的方式去创建线程处理的话,那么将导致系统的性能表现的非常糟糕。
  • 主要的弊端有这些
    • 大量的线程创建、执行和销毁是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM
    • 使用start()方法启动线程,该线程会在run()方法结束后,自动回收该线程。虽然如此,在某些场景中线程业务的处理速度完全达不到我们的要求,系统中的线程会逐渐变大,进而消耗CPU资源,大量的线程抢占宝贵的内存资源,可能还会出现OOM,即便没有出现,大量的线程回收也会个GC带来很大的压力。
    • 大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿
    • 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失

4.2 Thread+Looper+handler

  • Android 提供了 Handler 机制来进行线程之间的通信,我们可以使用 Android 最基础的异步方式:Thread + Looper + handler 来进行异步任务。
  • 优点:
    • 操作简单,无学习成本。
  • 缺点:
    • 代码规范性较差,不易维护。每次操作都会开启一个匿名线程,系统开销较大。
    • 多任务同时执行时不易精确控制线程。
  • 使用范围:
    • 多个异步任务的更新UI

4.3 AsyncTask

  • 较为轻量级的异步类
    • 封装了 FutureTask 的线程池、ArrayDeque 和 Handler 进行调度。AsyncTask 主要用于后台与界面持续交互。便于执行后台任务以及在子线程中进行UI操作。
  • 优点:
    • 结构清晰,使用简单,适合后台任务的交互。
    • 异步线程的优先级已经被默认设置成了:THREAD_PRIORITY_BACKGROUND,不会与 UI 线程抢占资源。
  • 缺点:
    • 结构略复杂,代码较多。每个 AsyncTask 只能被执行一次,多次调用会发生异常。
    • AsyncTask 在整个 Android 系统中维护一个线程池,有可能被其他进程的任务抢占而降低效率。
  • 使用范围
    • 单个异步任务的处理

4.4 HandlerThread

  • HandlerThread 是一个自带 Looper 消息循环的线程类。
    • 处理异步任务的方式和 Thread + Looper + Handler 方式相同。
  • 优点:
    • 简单,内部实现了普通线程的 Looper 消息循环。可以串行执行多个任务。
    • 内部拥有自己的消息队列,不会阻塞 UI 线程。
  • 缺点:
    • 没有结果返回接口,需要自行处理。消息过多时,容易造成阻塞。
    • 只有一个线程处理,效率较低。线程优先级默认优先级为 THREAD_PRIORITY_DEFAULT,容易和 UI 线程抢占资源。

4.5 IntentService

  • IntentService 继承自 Service 类。
    • 用于启动一个异步服务任务,它的内部是通过 HandlerThread 来实现异步处理任务的。是一种异步、会自动停止的服务。
  • 优点:
    • 只需要继承 IntentService,就可以在 onHandlerIntent 方法中异步处理 Intent 类型任务了。
    • 任务结束后 IntentService 会自行停止,无需手动调用 stopService。
    • 可以执行处理多个 Intent 请求,顺序执行多任务。
    • IntentService 是继承自 Service,具有后台 Service 的优先级。
  • 缺点:
    • 需要启动服务来执行异步任务,不适合简单任务处理。
    • 异步任务是由 HandlerThread 实现的,只能单线程、顺序处理任务。
    • 没有返回 UI 线程的接口。

4.6 线程池Executors

  • 利用 Executors 的静态方法创建多线程
    • java当中主要使用Thread和Executor来实现多线程. Thread用于直接创建线程, 在Android中也可以直接使用这个类, Looper中就包含一个Thread实例. Executor是一个接口, 大部分java中自带的实现都使用了线程池来管理多线程。
    • newCachedThreadPool()、newFixedThreadPool()、newSingleThreadExecutor() 及重载形式实例化 ExecutorService 接口即得到线程池对象。
  • 优点:
    • 线程的创建和销毁由线程池来维护,实现了线程的复用,从而减少了线程创建和销毁的开销。
    • 适合执行大量异步任务,提高性能。
    • 灵活性高,可以自由控制线程数量。
    • 扩展性好,可以根据实际需要进行扩展。
  • 缺点:
    • 代码略显复杂。线程池本身对系统资源有一定消耗。
    • 当线程数过多时,线程之间的切换成本会有很大开销,从而使性能严重下降。
    • 每个线程都会耗费至少 1040KB 内存,线程池的线程数量需要控制在一定范围内。
    • 线程的优先级具有继承性,如果在 UI 线程中创建线程池,线程的默认优先级会和 UI 线程相同,从而对 UI 线程使用资源进行抢占。

05.如何检测线程

5.1 线程检测方式

  • 第一种方式:使用Studio Profiler工具
    • 该工具优点:使用方便,实时性强,线程统计完整。缺点:无法对线程状态进行分组查看。
  • 第二种方式:使用adb命令,adb shell ps | grep
    • 该工具优点:线程统计完整。缺点:命令行方式使用不变,可读性比较差。
  • 第三种方式:Thread.getAllStackTraces()
    • 该工具优点:使用方便,返回信息已经封装。缺点:获取到的是当前线程组的线程,不是很完整。
  • 第四种方式:读取伪文件系统的线程信息
    • 该工具优点:实时性较高,线程统计完整。缺点:需要对文件内容进行解析。

5.2 Profiler检测线程

5.3 ADB获取检测线程

  • 首先查看进程
    • 通过命令查看App存在的进程,主要是获取uid数据:adb shell ps | grep com.zuoyebang.iotunion
  • 然后通过进程uid获取线程
    • 通过命令行初步查看uid对应进程的线程:adb shell ps -T | grep u0_a579
  • 分析进程中创建的线程
    • 这些线程有系统的线程,系统的线程占了很大一部分。剩下就是业务自己创建的线程以及三方库创建的线程,接下来就是要查找出线程的来源。
    • 创建线程就需要调用线程的构造方法,所以可以hook线程的构造方法,然后获取堆栈,这样就能知道线程的来源。

5.4 getAllStackTraces获取线程

5.5 读取伪文件检测线程

06.如何去统计线程

  • 如何使用命令行看字节码
    • 首先使用命令行输入:javac Xx.java。然后会编译出一个后缀名为.class的文件。
    • 接着通过javap查看字节码,这里输入命令:javap -c Xx。

6.1 创建线程的方式

  • 创建线程的方式
    • 第一种:Thread
    • 第二种:Timer
    • 第三种:HandlerThread
    • 第四种:AsyncTask
  • 了解线程创建和对应的字节码
  • 普通线程创建的代码如下所示
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
    
        }
    }, "job");
  • 该源码对应的字节码如下所示
    class java/lang/Thread
    Method java/lang/Thread."<init>": (Ljava/lang/Runnable;Ljava/lang/String;)V
    Method java/lang/Thread.start:()V

6.2 创建线程池的方式

  • 创建线程池的方式
    • 第一种:ThreadPoolExecutor
    • 第二种:Executors
    • 第三种:ScheduledThreadPoolExecutor
  • 了解线程池创建和对应的字节码
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
            Runtime.getRuntime().availableProcessors() * 2,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(),
            new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(true);
                    return thread;
                }
            });
  • 该源码对应的字节码如下所示
    class java/util/concurrent/ThreadPoolExecutor
    Method java/util/concurrent/ThreadPoolExecutor."<init>":(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;)V

6.3 扫描线程和线程池

  • 如何扫描线程和线程池
    • 扫描工具:可以使用ASM工具
    • 扫描条件:匹配方法名,方法描述等等
    • 扫描结果:按照不同lib库分类和统计
  • 如何选择合适技术
    • 插桩的框架,我选择的是ASM,因为使用ASM进行插桩具有高效性、灵活性、易用性、兼容性和社区活跃等优点,是一种比较优秀的字节码操作框架,对于提高应用程序的性能和可维护性具有重要意义。
  • 那么通过ASM是如何扫描到的呢?
    • 要扫描到创建线程池的类名,你需要使用ASM的访问者模式(Visitor Pattern)来遍历字节码中的方法和指令。
    • 在遍历过程中,当遇到创建线程的指令(如:new java/util/concurrent/ThreadPoolExecutor)时,就可以获取到创建线程的类名。

6.4 扫描结果统计分析

07.线程优化思路

7.1 优化线程思路

  • 对于APP业务层和自研SDK
    • 检查是否真的需要直接new thread,能否用线程池代替,如果必须创建单个线程,那我们创建的时候必须加上线程名,方便排查线程问题。

7.2 优化线程池思路

  • 对于APP业务层
    • 需要提供常用线程池,例如I/O、CPU、Single、Cache等等线程池,避免开发各自创建重复的线程池。
  • 对于自研SDK
    • 尽量让架构组的开发同学提供可以设置自定义线程池的能力,方便我们代理到我们APP业务层的线程池。
  • 对于三方SDK
    • 进行插桩来进行线程池收敛。在进行三方SDK插桩代理的时候,需要注意三点:
    • 设置白名单,进行逐步代理。
    • 针对不同的SDK,要区分是本地任务还是网络任务,这样能明确是代理到I/O线程池还是CPU线程池。
    • 设置降级开关,方便线上有问题时,及时对单个SDK进行降级处理。

7.3 行业优化方案思路

  • 反射收敛,但是使用反射来收敛线程池的确有一些潜在的弊端
    • 性能开销:反射在执行时需要进行一系列的检查和解析,这会比直接的Java方法方法调用带来更大的性能开销。
    • 安全问题:反射可以访问所有的字段和方法,包括私有有的和受保护的,这可能会破坏对象的封装性,导致安全问题。
    • 代码复杂性:使用反射的代码通常比直接的Java代码更复杂,更难理解和维护。
  • 代理收敛,但是使用代理设计模式来收敛线程池也有一些潜在的弊端
    • 增加复杂性:代理方式会引入额外的类和对象,这会增加系统的复杂性。对于简单的问题,使用代理可能会显得过于复杂。
    • 代码可读性:由于代理方式涉及到额外的抽象层,这可能会对代码的可读性产生一定的影响。
    • 调试困难:由于代理模式的存在,错误可能会被掩盖或者难以定位,这可能会使得调试变得更加困难。
  • 插桩收敛,虽然插桩也有一些不足之处,但也有不少优点
    • 可能影响程序行为:如果插桩代码改变了程序的状态或者影响了线程的线程的调度,那么它可能会改变程序的行为。
    • 可能引入错误:如果插桩代码桩代码本身存在错误,那么它可能会引入新的错误到程序中。
    • 直接性:插桩直接在代码中插入额外的逻辑,不需要通过代理或反射射间接地操作对象,这使得插桩更直接,更易于理解和控制。
    • 灵活活性:插桩可以在任何位置插入代码,,这提供了很大的灵活性。而代理和反射通常只能操作公开的接口和方法。
    • 无需修改原始代码:插桩通常常不需要常不需要修改原始的线程池代码,这使得它可以在不影响原始代码的情况下收集信息。
    • 颗粒度控制:可以对某个方法或某段代码进行线程收敛,而不是整个应用程序。

08.高效的使用线程

8.1 多线程安全问题

  • 线程安全问题
    • 使用多线程时需要注意的是线程安全的问题, 因为同一进程中的线程可以共享内存, 虽然这种方式效率很高, 但是会导致线程干扰和内存一致性的问题。
  • 锁机制
    • 解决这些问题的主要方法是使用Synchronized关键字来加锁. 基本原理就是线程要对对象进行操作前需要先获取锁, 如果一个线程正在操作某个对象, 那么它就会持有相应的锁, 后来的线程想要操作这个对象, 只能等待前面的线程释放锁之后才有机会获取锁并进行操作.

8.2 频繁发送消息问题

  • 频繁发送消息,处理消息不靠谱的,引起不靠谱的原因有如下
    • 发送的消息太多,Looper负载越高,任务越容易积压,进而导致卡顿
    • 消息队列有一些消息处理非常耗时,导致后面的消息延时处理
    • 大于Handler Looper的周期时基本可靠(例如主线程>50ms)
    • 对于时间精确度要求较高,不要用handler的delay作为即时的依据
  • 如何优化保证可靠性
    • 消息精简,从数量上处理
    • 队列优化,重复消息过滤
    • 互斥消息取消
    • 复用消息
  • 消息空闲IdleHandler
    MessageQueue.IdleHandler ideHandler =new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                return false;
            }
        };
    Looper.myQueue().addIdleHandler(ideHandler);
  • 使用独享的Looper(HandlerThread)
    HandlerThread handlerThread = new HandlerThread("A-Thread");
    handlerThread.start();
    Handler handler = new Handler(handlerThread.getLooper());

8.6 排查线程CPU使用率飙升

  • 首先模拟一个CPU高使用率场景
    public class HighCpuTest {
        public static void main(String[] args) {
            List<HignCpu> cpus = new ArrayList<>();
            Thread highCpuThread = new Thread(()->{
                int i = 0;
                while (true){
                    HignCpu cpu = new HignCpu("Java日知录",i);
    
                    cpus.add(cpu);
                    System.out.println("high cpu size:" + cpus.size());
                    i ++;
                }
            });
            highCpuThread.setName("HignCpu");
            highCpuThread.start();
        }
    }
  • 在main方法中开启了一个线程,无限构建HighCpu对象。
    @Data
    @AllArgsConstructor
    public class HignCpu {
        private String name;
        private int age;
    }
  • 排查步骤
    • 第一步,使用 top 找到占用 CPU 最高的 Java 进程。这一步监控cpu运行状,显示进程运行信息列表。
    • 第二步,用 top -Hp 命令查看占用 CPU 最高的线程。
      • 那一个进程中有那么多线程,不可能所有线程都一直占着 CPU 不放,这一步要做的就是揪出这个罪魁祸首,当然有可能不止一个。执行top -Hp pid命令,pid 就是前面的 Java 进程。然后再查查到占用CPU最高的那个线程PID。
    • 第三步,查看堆栈信息,定位对应代码。
      • 通过printf命令将其转化成16进制,之所以需要转化为16进制,是因为堆栈里,线程id是用16进制表示的。得到线程id后然后通过jstack命令查看堆栈信息。
      • 找到了耗CPU高的线程对应的线程名称“HighCpu”,以及看到了该线程正在执行代码的堆栈。最后,根据堆栈里的信息,定位到对应死循环代码,搞定。

09.优化线程的使用

9.1 设置线程优先级方式

  • 在 Android 中,有两种常见的设置线程优先级的方式:
    • 第一种,使用 Thread 类实例的 setPriority 方法,来设置线程优先级。
    • 第二种,使用 Process 类的 setThreadPriority 方法,来设置线程优先级。
  • 这两种设置线程的优先级,一种是 Java 原生版本,另一种是 Android API 版本。
    • 这两种方式是不同的,Android 更推荐使用第二种方式。
  • https://blog.csdn.net/u011578734/article/details/110549238

9.2 避免Thread直接创建

10.线程卡顿是怎么回事

10.1 线程如何卡顿

10.2 如何模拟线程卡顿

10.3 如何解决线程卡顿

参考内容

  • 线程卡顿耗时监控项目
    • MonitorThread,线程阻塞监控库
  • 轻量级统一线程池库
    • EasyExecutor,
  • 异步线程池库
    • ThreadPoolLib,
  • 线程Debug调试库
    • ThreadDebugLib,
  • HandlerThread库
    • HandlerThread,

1

  • 思考一些问题
    • 了解了如何创建线程,来执行异步任务,那么如果大量的使用线程,会不会影响 UI 线程呢?会不会产生卡顿呢?答案是肯定的。
    • 因为用户手机的 CPU 资源是有限的,内存也是有限的,如果无节制的同一时刻创建的大量的线程,就会导致线程和 UI 线程同时抢占 CPU 资源,造成 UI 线程执行变慢,产生卡顿等问题;
    • 并且线程创建是有代价的,线程不但占用了大量的 CPU 资源,同时也占用了大量的内存资源,Android 在真正创建线程时,会为每个线程申请 1040KB 的内存资源,大量的线程就有可能导致 OOM 等问题。
    • 另外,线程的创建和销毁也会占用系统资源来执行,所以应该合理的使用线程。
  • 如何高效的使用线程
    • 将异步任务分类,根据任务的紧急程度,使用不同优先级的线程来执行。
    • 将紧急任务,交给优先级高的线程来执行,并且要注意,线程不能产生长时间阻塞,否则会影响后续任务的执行。
    • 将不紧急任务,交给低优先级线程来执行,并且控制线程数量,少量线程,慢慢执行即可。例如,可以创建一个类似于 AnsyncTask 的全局的单线程任务队列,使用 1 个线程来执行一些不紧急的任务。
    • 使用线程池来优化线程的创建和管理线程。我们可以使用线程池来全局的管理线程的创建和执行,以此来避免频繁的线程创建和销毁,提高系统的性能。
    • 不要使用默认的线程优先级。通常,新创建的线程的线程优先级,默认情况下继承了父线程的线程优先级的,例如,在 UI 线程中创建了一个子线程,那么子线程的线程优先级就和 UI 线程是相同的。
    • 尽可能的将线程优先级级别设置的低一些,以避免子线程和 UI 线程竞争 CPU 资源。
    • 设置线程优先级时,应注意线程优先级的设置方法。

参考博客

  • 线程监控 - 死锁、存活周期与 CPU 占用率
    • https://www.jianshu.com/p/a4efccd09e02
  • Android性能优化-线程监控与线程统一
    • https://juejin.cn/post/7143944351016550437
  • Android线程优化这件事
    • https://juejin.cn/post/7287913415812202507
  • 关于 Android 线程优化你应该了解的知识点
    • https://juejin.cn/post/7130110976472383496
贡献者: yangchong211
上一篇
08.网络分析与优化实践
下一篇
10.高性能图片优化方案