Booster 系列之——多线程优化
項目地址:github.com/didi/booste…
對于開發者來說,線程管理一直是最頭疼的問題之一,尤其是業務復雜的 APP,每個業務模塊都有著幾十甚至上百個線程,而且,作為業務方,都希望本業務的線程優先級最高,能夠在調度的過程中獲得更多的 CPU 時間片,然而,過多的競爭意味著過多的資源浪費在了線程調度上。
如何能有效的解決上述的多線程管理問題呢?大多數人可能想到的是「使用統一的線程管理庫」,當然,這是最理想的情況,而往往現實并非總是盡如人意。隨著業務的高速迭代,積累的技術債也越來越多,面對錯綜復雜的業務邏輯和歷史遺留問題,架構師如何從容應對?
在此之前,我們通過對線程進行埋點監控,發現了以下的現象:
這些現象最終導致的問題是:
針對這些問題,如果采用上面提到的「統一線程管理庫」的方案,對于業務方來說,任何大范圍的改造都意味著風險和成本,那有沒有低成本的解決方案呢?經過反復思考和論證,最終我們選擇了字節碼注入方案,具體思路是:
對線程進行重命名
重命名線程的主要目的是為了區分該線程是由哪個模塊、哪個業務線創建的,這樣,線程監控埋點的聚合能夠做到更加精確
對線程池的參數進行調優
- 限制線程池的 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],并且,允許核心線程在空閑時銷毀,避免空閑線程占用過多的內存資源。
JDK Bug
經過以上對線程池的優化后中,我們信心滿滿的的準備灰度發布,但是,當我們在進行功耗測試時,發現 CPU 負載異常竟然高達 60%以上,經過一步步排查,最終發現問題出在 ScheduledThreadPool 的 minPoolSize 上,竟然命中了 JDK 的兩個 bug,而且這兩個 bug 直到 JDK 9 才修復:
- JDK-8022642
- JDK-8129861
這也就是為什么我們將 ScheduledThreadPool 的 minPoolSize 設置為了 1 的原因。
總結
針對多線程的優化主要是以下兩個關鍵點:
當然,以上的優化方案比較偏保守,主要是考慮到盡可能降低優化帶來的副作用,這也跟 APP 的應用場景有關,大家可以根據自身的業務需求進行相應的調整。
??
轉載于:https://juejin.im/post/5cfcdd2ee51d455071250ae2
總結
以上是生活随笔為你收集整理的Booster 系列之——多线程优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 20220213-CTF MISC-a_
- 下一篇: 命令执行漏洞-命令执行-漏洞位点- 代码