Booster 系列之——多線程優化

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

對于開發者來說,線程管理一直是最頭疼的問題之一,尤其是業務復雜的 APP,每個業務模塊都有著幾十甚至上百個線程,而且,作為業務方,都希望本業務的線程優先級最高,能夠在調度的過程中獲得更多的 CPU 時間片,然而,過多的競爭意味著過多的資源浪費在了線程調度上。

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

在此之前,我們通過對線程進行埋點監控,發現了以下的現象:

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

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

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

針對這些問題,如果采用上面提到的「統一線程管理庫」的方案,對于業務方來說,任何大范圍的改造都意味著風險和成本,那有沒有低成本的解決方案呢?經過反復思考和論證,最終我們選擇了字節碼注入方案,具體思路是:

  1. 對線程進行重命名

    重命名線程的主要目的是為了區分該線程是由哪個模塊、哪個業務線創建的,這樣,線程監控埋點的聚合能夠做到更加精確

  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 的來歷。

線程池參數優化

理解了線程重命名的實現原理,線程池參數優化也就能理解了,同樣也是將調用 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 才修復:

  • JDK-8022642
  • JDK-8129861

這也就是為什么我們將 ScheduledThreadPoolminPoolSize 設置為了 1 的原因。

總結

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

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

當然,以上的優化方案比較偏保守,主要是考慮到盡可能降低優化帶來的副作用,這也跟 APP 的應用場景有關,大家可以根據自身的業務需求進行相應的調整。


  1. MAX\_POOL\_SIZE = NCPU + 1??

轉載于:https://juejin.im/post/5cfcdd2ee51d455071250ae2

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/448649.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/448649.shtml
英文地址,請注明出處:http://en.pswp.cn/news/448649.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenCL

OpenCL&#xff08;全稱Open Computing Language&#xff0c;開放運算語言&#xff09;是第一個面向異構系統通用目的并行編程的開放式、免費標準&#xff0c;也是一個統一的編程環境&#xff0c;便于軟件開發人員為高性能計算服務器、桌面計算系統、手持設備編寫高效輕便的代碼…

dubbo的底層原理

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、Duboo基本概念解釋 Dubbo是一種分布式服務框架。 Webservice也是一種服務框架&#xff0c;但是webservice并不是分布式的服務框架&…

BOM屬性對象方法

本文原鏈接&#xff1a;https://cloud.tencent.com/developer/article/1018747 BOM 1.window對象 2.location對象 3.history對象 BOM也叫瀏覽器對象模型&#xff0c;它提供了很多對象&#xff0c;用于訪問瀏覽器的功能。BOM缺少規范&#xff0c;每個瀏覽器提供商又按照自己想法…

nginx+php+mysql+haproxy+keepalived+NFS,搭建wordpress

實現LNMP 實現環境&#xff1a; 服務版本系統CentOS7.6Mysql5.6.34Nginx1.14.2PHP7.1.30HAProxy1.8.20Keepalived1.3.5NFS1.3.0主機IPMysql_master192.168.37.108Mysql_slave192.168.37.105NginxPHP192.168.37.103NginxPHP192.168.37.104HAProxyKeepalived192.168.37.101HAPro…

OpenCL “速成”沖刺【第一天】

話說軟件開發從來沒有速成一說&#xff0c;一門語言你學的越快&#xff0c;說明你在別的語言上下個功夫越多&#xff0c;所以這次加了引號&#xff0c;只不過幾周之后可能會有一個公司內部OpenCL的考核&#xff0c;雖然本人不需要考核&#xff0c;不過也正好借機整理下之前Open…

Java8函數式編程

最近使用lambda表達式&#xff0c;感覺使用起來非常舒服&#xff0c;箭頭函數極大增強了代碼的表達能力。于是決心花點時間深入地去研究一下java8的函數式。 一、lambda表達式 先po一個最經典的例子——線程 public static void main(String[] args) {// Java7new Thread(new R…

電腦如何獲得管理員權限

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我只是記錄下&#xff0c;方便以后查看。 參見&#xff1a; https://jingyan.baidu.com/article/ab69b270ff426e2ca6189f54.html

.NET混淆器 Dotfuscator如何保護應用程序?控制流了解一下!

Dotfuscator是一個.NET的Obfuscator。它提供企業級的應用程序保護&#xff0c;大大降低了盜版、知識產權盜竊和篡改的風險。Dotfuscator的分層混淆、加密、水印、自動失效、防調試、防篡改、報警和防御技術&#xff0c;為世界各地成千上萬的應用程序提供保護。 Dotfuscator提供…

到底什么才是人生最大的投資

不是房子&#xff0c;不是股票&#xff0c; 是人&#xff0c;跟什么人交往&#xff0c;跟隨什么人&#xff0c; 交什么樣的朋友&#xff0c;其實就是你投資什么人&#xff0c; 而這&#xff0c;是對人生影響最大的。 錢不會給人機會&#xff0c;房子也不會&#xff0c; 只有人會…

tcpdump抓包命令

目錄&#xff1a; 命令格式選項expression表達式示例【命令格式】 man手冊顯示如下 1 tcpdump [ -AbdDefhHIJKlLnNOpqStuUvxX# ] [ -B buffer_size ]2 [ -c count ]3 [ -C file_size ] [ -G rotate_seconds ] [ -F file ]4 [ -i …

百度Ueditor編輯器wordimage踩坑

背景 改造公司老項目后臺編輯器&#xff0c;使用百度的Ueditor做替換。 發現問題 1、ue編輯器初始化后部分參數無法覆蓋ueditor.config.js中的選項。2、wordimage&#xff08;word圖片轉存&#xff09;始終是灰色&#xff0c;無法使用。解決辦法 1、將ueditor.config.js中的inp…

IntelliJ IDEA 配置JDK

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 IDEA配置JDK 1、點擊File -->Project Structure&#xff1b; 2、點擊左側標簽頁SDKs選項&#xff0c;再點擊左上角“”&#xff0c;…

get和post 兩種基本請求方式的區別

GET和POST是HTTP請求的兩種基本方法&#xff0c;要說它們的區別&#xff0c;接觸過WEB開發的人都能說出一二。 最直觀的區別就是GET把參數包含在URL中&#xff0c;POST通過request body傳遞參數。 你可能自己寫過無數個GET和POST請求&#xff0c;或者已經看過很多權威網站總結出…

無論是工作還是生活都要記住這些話

1.如果你不喜歡現在的工作&#xff0c;要么辭職不干&#xff0c;要么就閉嘴不言。初出茅廬&#xff0c;往往眼高手低&#xff0c;心高氣傲&#xff0c;大事做不了&#xff0c;小事不愿做。不要養成挑三揀四的習慣。不要雨天煩打傘&#xff0c;不帶傘又怕淋雨&#xff0c;處處表…

蘇嵌第一天,shell中一些基礎知識

一、常用環境變量 1、HOME變量 Linux系統中的每個用戶都有一個相關的稱作HOME的目錄。 2、PATH變量 包含一列用冒號定界的目錄的路徑名字&#xff0c;便于可執行程序的搜索。 3、PS1變量 PS1變量包含了shell提示符&#xff0c;$符號 4、LOGNAME變量 包含用戶的注冊名字…

Java異常處理001:Maven clean package時Failed to clean project: Failed to delete

Java異常處理001&#xff1a;Maven打包時Failed to clean project: Failed to delete 異常日志&#xff1a; [ERROR] Failed to execute goal org.apache.maven.plugins:maven-clean-plugin:2.6.1:clean (default-clean) on project fmk-web: Failed to clean project: Failed …

Weekly Contest 141

做了第一道后&#xff0c;看了下中間兩道題目&#xff0c;沒怎么看懂就先放著&#xff0c;做完最后一道&#xff0c;然后就沒時間了。 1089. Duplicate Zeros Given a fixed length array arr of integers, duplicate each occurrence of zero, shifting the remaining element…

IntelliJ IDEA 中配置、使用 SVN

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1.配置svn 如下圖&#xff1a; file -- setting -- version control -- subversion -- 選擇 SVN安裝路徑 -- apply -- OK 2.直接檢出…

切記!職場郵件需注意的細節

電子郵件是如今工作場所重要的通信工具之一&#xff0c;但不是每個人都知道如何很好地使用這個工具。工作郵件也是人際溝通的一種方式&#xff0c;和打電話、面談一樣&#xff0c;有很多學問講究&#xff0c;所以在發送郵件之前一定要深思熟慮。 【發送&#xff0c;抄送&…

李洋瘋狂C語言之初

1.sizeof 是看數據類型所占空間大小&#xff0c;這個大小是以 字節&#xff08;B&#xff09;為單位 char 是C語言的字符數據類型 %d 用在printf 中表示往屏幕打印一個數字 printf ("char&#xff1a; %d\n", sizeof(char)); 數據類型之間的關系&#xff0c;shor…