Booster 系列之——多線程優化

項目地址:github.com/didi/booste…java

對於開發者來講,線程管理一直是最頭疼的問題之一,尤爲是業務複雜的 APP,每一個業務模塊都有着幾十甚至上百個線程,並且,做爲業務方,都但願本業務的線程優先級最高,可以在調度的過程當中得到更多的 CPU 時間片,然而,過多的競爭意味着過多的資源浪費在了線程調度上。android

如何能有效的解決上述的多線程管理問題呢?大多數人可能想到的是「使用統一的線程管理庫」,固然,這是最理想的狀況,而每每現實並不是老是盡如人意。隨着業務的高速迭代,積累的技術債也愈來愈多,面對錯綜複雜的業務邏輯和歷史遺留問題,架構師如何從容應對?git

在此以前,咱們經過對線程進行埋點監控,發現瞭如下的現象:github

  1. 在某種場景下會無限制的建立新線程,最終致使 OOM
  2. 在某一時間應用內的線程數達到數百甚至上千
  3. 即便在空閒的時候,線程池中的線程一直在 WAITING ,一直不會銷燬

這些現象最終致使的問題是:markdown

  1. OOM
  2. 沒法分辨出線程所屬的業務線,致使排查問題效率低下

針對這些問題,若是採用上面提到的「統一線程管理庫」的方案,對於業務方來講,任何大範圍的改造都意味着風險和成本,那有沒有低成本的解決方案呢?通過反覆思考和論證,最終咱們選擇了字節碼注入方案,具體思路是:多線程

  1. 對線程進行重命名架構

    重命名線程的主要目的是爲了區分該線程是由哪一個模塊、哪一個業務線建立的,這樣,線程監控埋點的聚合可以作到更加精確oop

  2. 對線程池的參數進行調優測試

    • 限制線程池的 minPoolSizemaxPoolSize
    • 容許核心線程在空閒的時候自動銷燬

線程重命名

通過分析發現,APP 中的線程建立主要是經過如下幾種方式:優化

  • Thread 及其子類
  • TheadPoolExecutor 及其子類、ExecutorsThreadFactory 實現類
  • 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 的來歷。

booster-transform-thread

線程池參數優化

理解了線程重命名的實現原理,線程池參數優化也就能理解了,一樣也是將調用 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],而且,容許核心線程在空閒時銷燬,避免空閒線程佔用過多的內存資源。

JDK Bug

通過以上對線程池的優化後中,咱們信心滿滿的的準備灰度發佈,可是,當咱們在進行功耗測試時,發現 CPU 負載異常居然高達 60%以上,通過一步步排查,最終發現問題出在 ScheduledThreadPoolminPoolSize 上,居然命中了 JDK 的兩個 bug,並且這兩個 bug 直到 JDK 9 才修復:

這也就是爲何咱們將 ScheduledThreadPoolminPoolSize 設置爲了 1 的緣由。

總結

針對多線程的優化主要是如下兩個關鍵點:

  1. 將目標方法調用指令替換爲注入的靜態方法調用
  2. 在靜態方法中構造優化過的線程、線程池實例並返回

固然,以上的優化方案比較偏保守,主要是考慮到儘量下降優化帶來的反作用,這也跟 APP 的應用場景有關,你們能夠根據自身的業務需求進行相應的調整。


  1. MAX\_POOL\_SIZE = NCPU + 1 ↩︎

相關文章
相關標籤/搜索