多线程优化

线程管理面临的挑战

对于开发者来说,线程管理一直是最头疼的问题之一,尤其是业务复杂的 APP,每个业务模块都有着几十甚至上百个线程,而且,作为业务方,都希望本业务的线程优先级最高,能够在调度的过程中获得更多的 CPU 时间片,然而,过多的竞争意味着过多的资源浪费在了线程调度上。

如何能有效的解决上述的多线程管理问题呢?大多数人可能想到的是「使用统一的线程管理库」,当然,这是最理想的情况,而往往现实并非总是尽如人意。随着业务的高速迭代,积累的技术债也越来越多,面对错综复杂的业务逻辑和历史遗留问题,架构师如何从容应对?

通过对线程进行埋点监控,发现了以下的现象:

  1. 在某种场景下会无限制的创建新线程,最终导致 OOM
  2. 在某一时间应用内的线程数达到数百甚至上千
  3. 即使在空闲的时候,线程池中的线程一直在 WAITING

这些现象最终导致的问题是:

  1. Out Of Memory
  2. 无法分辨出线程所属的模块,导致排查问题困难

常规的优化方案

对于线程管理,常规的作法是:

  1. 使用统一的线程管理库,这种方案的优缺点都很明显,优点是可操作性强,很容易落地,缺点是优化不够彻底,对第三方类库无从管理;
  2. 有的团队甚至禁止在代码中使用 new Thread(),不然 Code Review 会被拒绝,但是 HandlerThread 也是 Thread,在 Android 中无可避免的会使用到;

无侵入的线程优化

booster-transform-thread在新窗口打开 提供了一种全新的思路——将所有创建线程的指令在编译期间替换成自定义的方法调用,经过大量的分析和验证后发现,Android 创建线程主要是通过以下几种方式:

  • Thread 及其子类
  • TheadPoolExecutor 及其子类、ExecutorsThreadFactory 实现类
  • AsyncTask
  • Timer 及其子类

优化思路

降低线程数量

降低线程数量并不是说不用线程或少用线程,而是让线程在空闲的时候自动销毁,基于这一理论,通过替换创建线程池的方法,或者直接修改线程池的参数:

  1. corePoolSize 设置为 0

    new ThreadPoolExecutor(0, maxCorePoolSize, keepAliveTime, timeUnit, workQueue, threadFactory);
    
    1

    注意

    ScheduledThreadPoolExecutorminPoolSize 设置为 0JDK 9 以下的版本会导致 CPU 负载严重,详见:JDK-8022642在新窗口打开, JDK-8129861在新窗口打开,这两个 bug 在 JDK 9 中被修复。

  2. maxPoolSize 设置上限

    有些开发者或者第三方库设置的线程池 maxPoolSize 通常是 2 * NCPU + 1 或者 2 * NCPU,当有多个模块都这样使用的时候,就容易造成某一时刻出现大量的线程,尤其是 CachedThreadPoolExecutor,通过控制单个线程池的 maxPoolSize 的上限,可以将某一时刻,所有线程池造成的叠加效应降到尽可能低的水平。

    new ThreadPoolExecutor(0, Math.min(maxCorePoolSize, NCPU), keepAliveTime, timeUnit, workQueue, threadFactory);
    
    1
  3. 允许核心线程在空闲时自动销毁

    executor.allowCoreThreadTimeOut(true)
    
    1
  4. 尽可能将 HandlerThread 替换成 SingleThreadPoolExecutor

    为什么不推荐用 HandlerThread?不可否认,Android 的 HandlerThread 确实简化了多线程开发,而实际的情况是,很少有开发者会主动销毁 HandlerThread,如果大量使用 HandlerThread 而不销毁,HandlerThread 会一直占用内存空间,每个线程栈大小至少是 1040k,如果大量的 HandlerThread 长时间处理 WAITING 状态,则会导致不必要的内存浪费,从而引发 Out Of Memory,采用 SingleThreadPoolExecutor 的好处是可以让核心线程在空闲时自动销毁。

为排查多线程问题提供线索

通过对线程进行重命名 —— 为线程名加上调用者的类名前缀,当 APM 工具上报异常信息或对线程进行采样时,采集到的线程信息对于排查问题会十分有帮助。

线程重命名

Thread 类为例,通过将 Thread 的构造方法调用替换成对应的自定义方法:

ThreadShadowThread
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();
    }

}
1
2
3
4
5
6
7
8
9
10
11

在未重命名之前,其创建的线程的命名是 Thread-{N},为了能让 APM 采集到的名字变成 com.didiglobal.booster.demo.MainActivity#Thread-{N} ,我们需要给线程的名字加一个前缀来标识,这个前缀就是 ShadowThread 的静态方法的最后一个 String 参数的来历。重命名后的效果如下图所示:

线程重命名

线程池优化

理解了线程重命名的实现原理,线程池参数优化也就能理解了,线程池优化主要做了这几件事情:

  1. 将调用 Executors 类的静态方法替换为 ShadowExecutors 的静态方法;
  2. 将调用 ThreadPoolExecutor 类的构造方法替换为 ShadowThreadPoolExecutor 的静态方法;
  3. Application 类的 <clinit>() 中调用 ShadowAsyncTask.optimizeAsyncTaskExecutor() 修改 AsyncTask 的线程池参数;

Executors 为例:

ExecutorsShadowExecutors
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
1

通过命令行配置

./gradlew assembleDebug -Pbooster.transform.thread.optimization.enabled=false
1

当禁用线程池优化后,Executors 的方法调用会被替换成 ShadowExecutors 中对应的非优化的方法,如下表所示:

ExecutorsShadowExecutors
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)