多线程优化
线程管理面临的挑战
对于开发者来说,线程管理一直是最头疼的问题之一,尤其是业务复杂的 APP,每个业务模块都有着几十甚至上百个线程,而且,作为业务方,都希望本业务的线程优先级最高,能够在调度的过程中获得更多的 CPU 时间片,然而,过多的竞争意味着过多的资源浪费在了线程调度上。
如何能有效的解决上述的多线程管理问题呢?大多数人可能想到的是「使用统一的线程管理库」,当然,这是最理想的情况,而往往现实并非总是尽如人意。随着业务的高速迭代,积累的技术债也越来越多,面对错综复杂的业务逻辑和历史遗留问题,架构师如何从容应对?
通过对线程进行埋点监控,发现了以下的现象:
- 在某种场景下会无限制的创建新线程,最终导致 OOM
- 在某一时间应用内的线程数达到数百甚至上千
- 即使在空闲的时候,线程池中的线程一直在 WAITING
这些现象最终导致的问题是:
- Out Of Memory
- 无法分辨出线程所属的模块,导致排查问题困难
常规的优化方案
对于线程管理,常规的作法是:
- 使用统一的线程管理库,这种方案的优缺点都很明显,优点是可操作性强,很容易落地,缺点是优化不够彻底,对第三方类库无从管理;
- 有的团队甚至禁止在代码中使用
new Thread()
,不然 Code Review 会被拒绝,但是 HandlerThread 也是 Thread,在 Android 中无可避免的会使用到;
无侵入的线程优化
booster-transform-thread在新窗口打开 提供了一种全新的思路——将所有创建线程的指令在编译期间替换成自定义的方法调用,经过大量的分析和验证后发现,Android 创建线程主要是通过以下几种方式:
Thread
及其子类TheadPoolExecutor
及其子类、Executors
、ThreadFactory
实现类AsyncTask
Timer
及其子类
优化思路
降低线程数量
降低线程数量并不是说不用线程或少用线程,而是让线程在空闲的时候自动销毁,基于这一理论,通过替换创建线程池的方法,或者直接修改线程池的参数:
将 corePoolSize 设置为 0
new ThreadPoolExecutor(0, maxCorePoolSize, keepAliveTime, timeUnit, workQueue, threadFactory);
1注意
将
ScheduledThreadPoolExecutor
的minPoolSize
设置为 0 在 JDK 9 以下的版本会导致 CPU 负载严重,详见:JDK-8022642在新窗口打开, JDK-8129861在新窗口打开,这两个 bug 在 JDK 9 中被修复。为
maxPoolSize
设置上限有些开发者或者第三方库设置的线程池
maxPoolSize
通常是2 * NCPU + 1
或者2 * NCPU
,当有多个模块都这样使用的时候,就容易造成某一时刻出现大量的线程,尤其是 CachedThreadPoolExecutor,通过控制单个线程池的maxPoolSize
的上限,可以将某一时刻,所有线程池造成的叠加效应降到尽可能低的水平。new ThreadPoolExecutor(0, Math.min(maxCorePoolSize, NCPU), keepAliveTime, timeUnit, workQueue, threadFactory);
1允许核心线程在空闲时自动销毁
executor.allowCoreThreadTimeOut(true)
1尽可能将 HandlerThread 替换成 SingleThreadPoolExecutor
为什么不推荐用 HandlerThread?不可否认,Android 的 HandlerThread 确实简化了多线程开发,而实际的情况是,很少有开发者会主动销毁 HandlerThread,如果大量使用 HandlerThread 而不销毁,HandlerThread 会一直占用内存空间,每个线程栈大小至少是 1040k,如果大量的 HandlerThread 长时间处理 WAITING 状态,则会导致不必要的内存浪费,从而引发 Out Of Memory,采用 SingleThreadPoolExecutor 的好处是可以让核心线程在空闲时自动销毁。
为排查多线程问题提供线索
通过对线程进行重命名 —— 为线程名加上调用者的类名前缀,当 APM 工具上报异常信息或对线程进行采样时,采集到的线程信息对于排查问题会十分有帮助。
线程重命名
以 Thread
类为例,通过将 Thread 的构造方法调用替换成对应的自定义方法:
Thread | ShadowThread |
---|---|
Thread() | newThread(String) |
Thread(Runnable) | newThread(Runnable, String) |
Thread(ThreadGroup, Runnable) | newThread(ThreadGroup, Runnable, String) |
Thread(String) | newThread(String, String) |
Thread(ThreadGroup, String) | newThread(ThreadGroup, String, String) |
Thread(Runnable, String) | newThread(Runnable, String, String) |
Thread(ThreadGroup, Runnable, String) | newThread(ThreadGroup, Runnable, String, String) |
Thread(ThreadGroup, Runnable, String, long) | newThread(ThreadGroup, Runnable, String, String) |
细心的读者可能会发现,ShadowThread
类的这些静态方法的参数比替换之前多了一个 String
类型的参数,其实,这个 String
参数就是调用 Thread
的构造方法的类的 className
,而这个类名,是在 Transform 的过程中扫描出来的,通过这个 className
来为 Thread 重命名,这样,就可以在运行时确定这个线程是从哪儿冒出来的。
下面用一个简单的例子来说明,假设,我们有一个 MainActivity
类:
public class MainActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
new Thread(new Runnable() {
public void run() {
doSomething();
}
}).start();
}
}
2
3
4
5
6
7
8
9
10
11
在未重命名之前,其创建的线程的命名是 Thread-{N},为了能让 APM 采集到的名字变成 com.didiglobal.booster.demo.MainActivity#Thread-{N} ,我们需要给线程的名字加一个前缀来标识,这个前缀就是 ShadowThread
的静态方法的最后一个 String
参数的来历。重命名后的效果如下图所示:
线程池优化
理解了线程重命名的实现原理,线程池参数优化也就能理解了,线程池优化主要做了这几件事情:
- 将调用
Executors
类的静态方法替换为ShadowExecutors
的静态方法; - 将调用
ThreadPoolExecutor
类的构造方法替换为ShadowThreadPoolExecutor
的静态方法; - 在
Application
类的<clinit>()
中调用ShadowAsyncTask.optimizeAsyncTaskExecutor()
修改AsyncTask
的线程池参数;
以 Executors
为例:
Executors | ShadowExecutors |
---|---|
newFixedThreadPool(int) | newOptimizedFixedThreadPool(int, String) |
newFixedThreadPool(int, ThreadFactory) | newOptimizedFixedThreadPool(int, ThreadFactory, String) |
newSingleThreadExecutor() | newOptimizedSingleThreadExecutor(String) |
newSingleThreadExecutor(ThreadFactory) | newOptimizedSingleThreadExecutor(ThreadFactory, String) |
newCachedThreadPool() | newOptimizedCachedThreadPool(String) |
newCachedThreadPool(ThreadFactory) | newOptimizedCachedThreadPool(ThreadFactory, String) |
newSingleThreadScheduledExecutor() | newOptimizedSingleThreadScheduledExecutor(String) |
newSingleThreadScheduledExecutor() | newOptimizedSingleThreadScheduledExecutor(String) |
newSingleThreadScheduledExecutor(ThreadFactory) | newOptimizedSingleThreadScheduledExecutor(ThreadFactory, String) |
newScheduledThreadPool(int) | newOptimizedScheduledThreadPool(int, String) |
newScheduledThreadPool(int, ThreadFactory) | newOptimizedScheduledThreadPool(int, ThreadFactory, String) |
禁用线程池优化
为了避免产生意想不到的副作用,Booster 同样支持仅对线程重命名,而不启用线程优化。
通过 gradle.properties 配置
booster.transform.thread.optimization.enabled=false
通过命令行配置
./gradlew assembleDebug -Pbooster.transform.thread.optimization.enabled=false
当禁用线程池优化后,Executors
的方法调用会被替换成 ShadowExecutors
中对应的非优化的方法,如下表所示:
Executors | ShadowExecutors |
---|---|
newFixedThreadPool(int) | newFixedThreadPool(int, String) |
newFixedThreadPool(int, ThreadFactory) | newFixedThreadPool(int, ThreadFactory, String) |
newSingleThreadExecutor() | newSingleThreadExecutor(String) |
newSingleThreadExecutor(ThreadFactory) | newSingleThreadExecutor(ThreadFactory, String) |
newCachedThreadPool() | newCachedThreadPool(String) |
newCachedThreadPool(ThreadFactory) | newCachedThreadPool(ThreadFactory, String) |
newSingleThreadScheduledExecutor() | newSingleThreadScheduledExecutor(String) |
newSingleThreadScheduledExecutor() | newSingleThreadScheduledExecutor(String) |
newSingleThreadScheduledExecutor(ThreadFactory) | newSingleThreadScheduledExecutor(ThreadFactory, String) |
newScheduledThreadPool(int) | newScheduledThreadPool(int, String) |
newScheduledThreadPool(int, ThreadFactory) | newScheduledThreadPool(int, ThreadFactory, String) |