项目地址:github.com/didi/booste…java
对于开发者来讲,线程管理一直是最头疼的问题之一,尤为是业务复杂的 APP,每一个业务模块都有着几十甚至上百个线程,并且,做为业务方,都但愿本业务的线程优先级最高,可以在调度的过程当中得到更多的 CPU 时间片,然而,过多的竞争意味着过多的资源浪费在了线程调度上。android
如何能有效的解决上述的多线程管理问题呢?大多数人可能想到的是「使用统一的线程管理库」,固然,这是最理想的状况,而每每现实并不是老是尽如人意。随着业务的高速迭代,积累的技术债也愈来愈多,面对错综复杂的业务逻辑和历史遗留问题,架构师如何从容应对?git
在此以前,咱们经过对线程进行埋点监控,发现了如下的现象:github
这些现象最终致使的问题是:markdown
针对这些问题,若是采用上面提到的「统一线程管理库」的方案,对于业务方来讲,任何大范围的改造都意味着风险和成本,那有没有低成本的解决方案呢?通过反复思考和论证,最终咱们选择了字节码注入方案,具体思路是:多线程
对线程进行重命名架构
重命名线程的主要目的是为了区分该线程是由哪一个模块、哪一个业务线建立的,这样,线程监控埋点的聚合可以作到更加精确oop
对线程池的参数进行调优测试
minPoolSize
和 maxPoolSize
通过分析发现,APP 中的线程建立主要是经过如下几种方式:优化
Thread
及其子类TheadPoolExecutor
及其子类、Executors
、ThreadFactory
实现类AsyncTask
Timer
及其子类以 Thread 类为例,能够经过如下构造方法进行线程的实例化:
Thread()
Thread(runnable: Runnable)
Thread(group: ThreadGroup, runnable: Runnable)
Thread(name: String)
Thread(group: ThreadGroup, name: String)
Thread(runnable: Runnable, name: String)
Thread(group: ThreadGroup, runnable: Runnable, name: String)
Thread(group: ThreadGroup, runnable: Runnable, name: String, stackSize: long)
咱们的目标就是将以上这些方法调用替换成对应的 ShadowThread 的静态方法:
ShadowThread.newThread(prefix: String)
public static Thread newThread(final String prefix) { return new Thread(prefix); } 复制代码
ShadowThread.newThread(target: Runnable, prefix: String)
public static Thread newThread(final Runnable target, final String prefix) { return new Thread(target, prefix); } 复制代码
ShadowThread.newThread(group: ThreadGroup, target: Runnable, prefix: String)
public static Thread newThread(final ThreadGroup group, final Runnable target, final String prefix) { return new Thread(group, target, prefix); } 复制代码
ShadowThread.newThread(name: String, prefix: String)
public static Thread newThread(final String name, final String prefix) { return new Thread(makeThreadName(name, prefix)); } 复制代码
ShadowThread.newThread(group: ThreadGroup, name: String, prefix: String)
public static Thread newThread(final ThreadGroup group, final String name, final String prefix) { return new Thread(group, makeThreadName(name, prefix)); } 复制代码
ShadowThread.newThread(target: Runnable, name: String, prefix: String)
public static Thread newThread(final Runnable target, final String name, final String prefix) { return new Thread(target, makeThreadName(name, prefix)); } 复制代码
ShadowThread.newThread(group: ThreadGroup, target: Runnable, name: String, prefix: String)
public static Thread newThread(final ThreadGroup group, final Runnable target, final String name, final String prefix) { return new Thread(group, target, makeThreadName(name, prefix)); } 复制代码
ShadowThread.newThread(group: ThreadGroup, target: Runnable, name: String, prefix: String)
public static Thread newThread(final ThreadGroup group, final Runnable target, final String name, final long stackSize, final String prefix) { return new Thread(group, target, makeThreadName(name, prefix), stackSize); } 复制代码
细心的读者可能会发现,ShadowThread
类的这些静态方法的参数比替换以前多了一个 prefix
,其实,这个 prefix
就是调用 Thread
的构造方法的类的 className,而这个类名,是在 Transform 的过程当中扫描出来的,下面用一个简单的例子来讲明,好比咱们有一个 MainActivity 类:
package com.didiglobal.booster.demo; public class MainActivity extends AppCompatActivity { public void onCreate(Bundle savedInstanceState) { new Thread(new Runnable() { public void run() { doSomething(); } }).start(); } } 复制代码
在未重命名以前,其建立的线程的命名是 Thread-{N},为了能让 APM 采集到的名字变成 com.didiglobal.booster.demo.MainActivity#Thread-{N},咱们须要给线程的名字加一个前缀来标识,这个前缀就是 ShadowThread
的静态方法的最后一个参数 prefix
的来历。
理解了线程重命名的实现原理,线程池参数优化也就能理解了,一样也是将调用 ThreadPoolExecutor
类的构造方法替换为 ShadowThreadPoolExecutor 的静态方法,以下所示:
public static ThreadPoolExecutor newThreadPoolExecutor(final int corePoolSize, final int maxPoolSize, final long keepAliveTime, final TimeUnit unit, final BlockingQueue<Runnable> workQueue, final String name) { final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, MAX_POOL_SIZE, keepAliveTime, unit, workQueue, new NamedThreadFactory(name)); executor.allowCoreThreadTimeOut(keepAliveTime > 0); return executor; } 复制代码
以上示例中,将线程池的核心线程数设置为 0
,最大线程数设置为 MAX_POOL_SIZE
[1],而且,容许核心线程在空闲时销毁,避免空闲线程占用过多的内存资源。
通过以上对线程池的优化后中,咱们信心满满的的准备灰度发布,可是,当咱们在进行功耗测试时,发现 CPU 负载异常居然高达 60%以上,通过一步步排查,最终发现问题出在 ScheduledThreadPool
的 minPoolSize
上,居然命中了 JDK 的两个 bug,并且这两个 bug 直到 JDK 9 才修复:
这也就是为何咱们将 ScheduledThreadPool
的 minPoolSize
设置为了 1
的缘由。
针对多线程的优化主要是如下两个关键点:
固然,以上的优化方案比较偏保守,主要是考虑到尽量下降优化带来的反作用,这也跟 APP 的应用场景有关,你们能够根据自身的业务需求进行相应的调整。