項目地址: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 的應用場景有關,你們能夠根據自身的業務需求進行相應的調整。