pthread_create ——我與華為線程的爭斗

首發(fā)于公眾號: DSGtalk1989

好久不見,值此年終之際,跟大家探討一下,一個詭異的內(nèi)存溢出。

羈絆與猜想

話不多說,先上崩潰

java.lang.OutOfMemoryError

pthread_create (1040KB stack) failed: Out of memory

解析原始
1 java.lang.Thread.nativeCreate(Native Method)
2 java.lang.Thread.start(Thread.java:1078)

很顯然,創(chuàng)建了一個線程,1040kb,內(nèi)存溢出。
正好我們比對一下一般的內(nèi)存溢出的情況是怎樣的。

java.lang.OutOfMemoryError

Failed to allocate a 1056012 byte allocation with 801854 free bytes and 783KB until OOM

當(dāng)再次分配內(nèi)存的時候出現(xiàn)了內(nèi)存溢出。
那么我會猜測以上兩種情況是否都是內(nèi)存不夠用了,或者說上面的是否和下面的一樣是因為內(nèi)存不夠用了引起的。
為什么恰好的創(chuàng)建線程的時候崩潰了,創(chuàng)建線程需要的內(nèi)存通常不會很大,就真的那么巧,這一根稻草壓垮了駱駝?

一筆帶過的結(jié)論

在此直接吐結(jié)論,因為探索pthread_create的內(nèi)存溢出原因,不是本文的主旨,本文主要探討是如何斗爭。

大家應(yīng)該都知道,線程創(chuàng)建的越多,資源就消耗的越大,android本身并沒有為我們限制應(yīng)用承受的最大線程數(shù),將控制權(quán)交由應(yīng)用設(shè)計者自己。

那么很明顯國內(nèi)的廠商,并不相信國內(nèi)的開發(fā),或者說國內(nèi)的廠商由于自己的定制導(dǎo)致你即使遵循了應(yīng)用的開發(fā)之道,卻依然難以避免產(chǎn)生大量的冗余線程。

因此華為帶頭出擊,魅族隨后,分別將自己的大量機型可控線程數(shù)調(diào)至500和3000。

如果你深表懷疑或者依然存有困惑,送你如下傳送門

  1. 不可思議的OOM
  2. 由一個stack OOM引發(fā)的血案

仗怎么打

首先我們需要將應(yīng)用中可能出現(xiàn)的所有與線程相關(guān)的地方找到,或者說找出應(yīng)用中可能會出現(xiàn)線程池的地方。

此處只舉個例,不做窮舉。

  • 網(wǎng)絡(luò)請求肯定是首當(dāng)其沖,如果你使用的市面流行的第三方框架,Retrofit + Okhttp肯定有默認的線程池問題,如果你在外面還包了一層Rxjava,那么在這一層面還有一個線程池問題。
    當(dāng)然,如果牛逼到自己封裝了自己的一套網(wǎng)絡(luò)請求,那么想必會考慮到多線程的問題,應(yīng)該如何挑選最合適的線程池來讓自己的效率最優(yōu)。

  • 項目是否使用了事件總線,同上,如果你使用了EventBus,那么這玩意兒幫我們使用了默認的線程池,如果你同樣自定義了一套完整的事件總線,那么你也同樣需要關(guān)注多線程的問題。

  • 如果項目較大,很有可能還會有自定義的Executor,全局搜索一下,看下是如何定義的線程池。

再看Executor

啥都不說,我們從構(gòu)造方法開始

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, 
long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
  • corePoolSize

    the number of threads to keep in the pool, 
    even if they are idle, unless {@code allowCoreThreadTimeOut} is set
    

    線程池中所保存的核心線程數(shù),包括空閑線程。除非設(shè)置了allowCoreThreadTimeOut屬性

    我們可以直接理解成,當(dāng)緩沖隊列中為空時,線程池中最大允許的線程數(shù)量,而且此時,只要有一個任務(wù)過來就會新建一個線程,直到達到核心線程數(shù)。

  • maximumPoolSize

    the maximum number of threads to allow in the pool
    

    線程池中允許的最大線程數(shù)
    你可以理解成線程池中允許的最大同時執(zhí)行線程數(shù)

  • keepAliveTime

    when the number of threads is greater than
    the core, this is the maximum time that excess idle threads
    will wait for new tasks before terminating.
    

    當(dāng)線程池中的線程數(shù)比核心線程數(shù)來的多時,空閑線程所能持續(xù)的最長時間。
    這個講的比較明確了。

  • unit

    the time unit for the {@code keepAliveTime} argument
    

    時間單位

  • workQueue

    the queue to use for holding tasks before they are
    executed.  This queue will hold only the {@code Runnable}
    tasks submitted by the {@code execute} method.
    

    任務(wù)執(zhí)行前保存任務(wù)的隊列,僅保存由 execute 方法提交的 Runnable 任務(wù)。

  • handler

    the handler to use when execution is blocked
    because the thread bounds and queue capacities are reached
    

    當(dāng)線程越界,且隊列阻塞了之后的處理類。

捋順Executor

當(dāng)來了一個新的任務(wù)

  1. 我們會首先去看線程池中是否有空閑的線程
  2. 如果線程池中當(dāng)前沒有線程,創(chuàng)建一個線程執(zhí)行任務(wù)
  3. 如果當(dāng)前線程池中線程小于核心線程數(shù),創(chuàng)建一個線程執(zhí)行任務(wù)
  4. 如果當(dāng)前線程池中線程數(shù)等于核心線程數(shù),將任務(wù)放入緩存隊列,等待線程執(zhí)行完畢,就將任務(wù)拋到執(zhí)行完任務(wù)的線程執(zhí)行。
  5. 如果當(dāng)前線程池中線程數(shù)等于核心線程數(shù),且緩存隊列已滿,且當(dāng)前線程池內(nèi)的線程數(shù)量小于最大線程數(shù),那么就創(chuàng)建一個線程出來執(zhí)行任務(wù)
  6. 如果當(dāng)前線程池線程數(shù)等于最大線程數(shù),那么會觸發(fā)handler,詢問該如何處理超過的線程數(shù),一旦沒有創(chuàng)建handler,handler不作處理,則直接拋出未處理異常。

總結(jié)來說就是 核心 > 隊列 > 非核心 > handler

Retrofit開刀

OK,當(dāng)我們真的搞清楚了,到底Executor是怎么一回事了之后,我們來拿Retrofit1RestAdapter看一下。

進到RestAdapter的內(nèi)部類Builder類。

public static class Builder {
  ...
     private void ensureSaneDefaults() {
            ...
            if(this.httpExecutor == null) {
                this.httpExecutor = Platform.get().defaultHttpExecutor();
            }
     }
}

如果你不去專門設(shè)置httpExecutor,那么Retrofit就會幫你設(shè)置一個默認的,具體是怎樣的線程池,我們繼續(xù)看。

private static class Android extends Platform {
        ...
        Executor defaultHttpExecutor() {
            return Executors.newCachedThreadPool(new ThreadFactory() {
                public Thread newThread(final Runnable r) {
                    return new Thread(new Runnable() {
                        public void run() {
                            Process.setThreadPriority(10);
                            r.run();
                        }
                    }, "Retrofit-Idle");
                }
            });
        }
        ...
}

直接去Platform的實現(xiàn)類Android,來找到底defaultHttpExecutor是個啥,一看newCachedThreadPool基本就明了了,使用了系統(tǒng)封裝好的緩存線程池,可以再細看一下:

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

按照我們上面分析的,核心線程為0個,使用同步隊列,SynchronousQueue是沒有容量的,此處不做詳解,按照目前的邏輯,只要是需要執(zhí)行的task,一旦put了會立馬被take,此處對同步隊列不做詳解,各位可以自行鉆研??梢哉J為一旦有任務(wù)進來,會立馬創(chuàng)建新的線程,如果線程空閑60秒就會被干掉。

因此,理論上來說,這個線程池在極端情況下可以產(chǎn)生Integer.MAX_VALUE個線程,那么當(dāng)這個數(shù)字直線上升的時候,就可以看到華為,魅族,相距的崩掉(出于本身ANR的保護,你可能很難看到3000的魅族崩,甚至其他的上萬限制的手機崩掉

因此為了避免此類情況的發(fā)生,我們需要給出自定義的線程池方案,你可以選擇有限的最大線程數(shù)配合SynchronousQueue隊列,或者選擇有限的最大線程數(shù)配合有限容量的BlockingQueue其他實現(xiàn)類。

比如

builder.setExecutors(new ThreadPoolExecutor(2, 20, 20L, 
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(64), 
new ThreadPoolExecutor.DiscardOldestPolicy()), null);

各位可以自己摸索鉆研到一套最適合自己應(yīng)用場景的線程池配置。
記住,一旦出現(xiàn)了有上限的線程池,必須要設(shè)置溢出策略,上面采用了new ThreadPoolExecutor.DiscardOldestPolicy()方式,該策略會當(dāng)線程池溢出時,拋掉緩存隊列中最先進去的那個,插入新來的。

后記

優(yōu)化線程數(shù),遠遠不止于此。

再比如不同的api域名,通常我們需要創(chuàng)建不同的Retrofit對象,而不同的Retrofit對象,都需要進行初始化,傳入不同的OkhttpClient和相同的OkhttpClient也會有很大的線程數(shù)差距。

重要的還是在平時的編碼過程中,善于觀察,善于測試,善于從崩潰日志中過濾無用信息,定位到線程崩潰點。

最后,為無法理解華為的線程限制抓耳撓腮10分鐘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評論 25 709
  • 【JAVA 線程】 線程 進程:是一個正在執(zhí)行中的程序。每一個進程執(zhí)行都有一個執(zhí)行順序。該順序是一個執(zhí)行路徑,或者...
    Rtia閱讀 2,890評論 2 20
  • layout: posttitle: 《Java并發(fā)編程的藝術(shù)》筆記categories: Javaexcerpt...
    xiaogmail閱讀 6,005評論 1 19
  • 某天找素材看到一滴水珠,很心動,當(dāng)晚就臨摹一個。昨天又一口氣臨摹兩個,很厲害有沒有?其實,畫一個水珠大概用五分鐘??...
    昭月兒閱讀 700評論 1 6
  • 這是人生第一幅油畫作品,半個小時,還是自由創(chuàng)作,感覺還不錯。 下面的是二號作品,三個小時。畫成水粉了。沒有美術(shù)工底...
    鞥咕閱讀 472評論 5 5

友情鏈接更多精彩內(nèi)容