從執行緒池理論聊聊為什麼要看原始碼

肥朝發表於2019-03-17

前言

很多時候,我都想向大家傳輸一個思想,那就是隻有懂了原理,才能隨心隨心所欲寫程式碼.而看原始碼,又是瞭解原理的一個非常重要的途徑.

然而,肥朝之前的文章,大致分為三類

第三點,我認為尤其重要.我們看原始碼的目的是為了解決問題,我覺得只談付出,不談回報都是耍流氓.如果只告訴大家要懂原理,看原始碼,接著貼幾大段原始碼,然後給大片大片的原始碼打上註釋.看了大段大段的註釋下來,好像都懂了,感覺很"充實".

但是我們要的並不是這種自我感覺的"充實",而是真真正正通過原始碼,解決了 搜尋無法解決的問題 ,只有這樣.才是有收穫的.如果百度隨便一搜都有答案的那你還捨近求遠的看原始碼這就實在是裝逼了

直入主題

今天在公司壓測的效能群,出現了這麼一個問題,如下圖:

從執行緒池理論聊聊為什麼要看原始碼

粗略一看,大概Dubbo執行緒池達到最大執行緒數丟擲的異常.那麼我們先來鋪墊執行緒池的知識基本儲備

常見執行緒池

  • SingleThreadExecutor: 單執行緒執行緒池,一般很少使用.

  • FixedThreadExecutor: 固定數量執行緒池,這個比較常用,重點留意一下,也是本文重點

  • CachedThreadExecutor: 字面翻譯快取執行緒池,這個也比較常用,重點留意一下,也是本文重點

  • ScheduledThreadExecutor: 定時排程執行緒池,一般很少使用.那這裡可能就有人反駁了.那為什麼Dubbo原始碼裡面的定時任務要用這個?看原始碼最重要的還是要看出別人的設計思想.Dubbo設計的初衷是只依賴JDK,使用他的定時任務,自然是優先選擇使用這個JDK原生的API來做一個簡易的定時任務.

執行緒池引數的意義及工作原理

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  //...
}
複製程式碼

執行緒池有這麼幾個重要的引數

corePoolSize: 執行緒池裡的核心執行緒數量

maximumPoolSize: 執行緒池裡允許有的最大執行緒數量

keepAliveTime: 如果 當前執行緒數量 > corePoolSize,多出來的執行緒會在keepAliveTime之後就被釋放掉

unit: keepAliveTime的時間單位,比如分鐘,小時等

workQueue: 佇列

threadFactory: 每當需要建立新的執行緒放入執行緒池的時候,就是通過這個執行緒工廠來建立的

handler: 就是說當執行緒,佇列都滿了,之後採取的策略,比如丟擲異常等策略

那麼我們假設來一組引數練習一下這個引數的意義

corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞佇列,佇列大小是4
handler:預設的策略,丟擲來一個ThreadPoolRejectException
複製程式碼

1.一開始有一個執行緒變數poolSize維護當前執行緒數量.此時poolSize=0

2.此時來了一個任務.需要建立執行緒.poolSize(0) < corePoolSize(1),那麼直接建立執行緒

3.此時來了一個任務.需要建立執行緒.poolSize(1) >= corePoolSize(1),此時佇列沒滿,那麼就丟到佇列中去

4.如果佇列也滿了,但是poolSize < mamximumPoolSize,那麼繼續建立執行緒

5.如果poolSize == maximumPoolSize,那麼此時再提交一個一個任務,就要執行handler,預設就是丟擲異常

6.此時執行緒池有3個執行緒(poolSize == maximumPoolSize(3)),假如都處於空閒狀態,但是corePoolSize=1,那麼就有(3-1 =2),那麼這超出的2個空閒執行緒,空閒超過60s,就會給回收掉.

以上,就是執行緒池引數意義及工作原理

執行緒池引數設計上的思考

知道了以上的原理,那麼我們看看常見的兩個執行緒池FixedThreadExecutorCachedThreadExecutor的引數設計

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
複製程式碼
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
複製程式碼

那麼問題來了

1.為什麼FixedThreadExecutorcorePoolSizemamximumPoolSize要設計成一樣的?

2.為什麼CachedThreadExecutormamximumPoolSize要設計成接近無限大的?

敲黑板劃重點

還是前面那句話,我們看原始碼,並不是大段大段的原始碼打上註釋,最重要的是經過深度思考,明白作者設計的意圖,這也就是為什麼市場上有這麼多原始碼解析文章,我們依然還要關注一下肥朝(賣個萌)

如果你對上面的執行緒池的原理,引數有了清晰的認識,自然很快就能明白這個設計思路.

比如問題一,因為執行緒池是先判斷corePoolSize,再判斷workQueue,最後判斷mamximumPoolSize,然而LinkedBlockingQueue是無界佇列,所以他是達不到判斷mamximumPoolSize這一步的,所以mamximumPoolSize成多少,並沒有多大所謂

比如問題二:我們來看看SynchronousQueue的註釋:

從執行緒池理論聊聊為什麼要看原始碼

從我圈的這幾個小學英文單詞都知道,這個佇列的容量是很小的,如果mamximumPoolSize不設計得很大,那麼就很容易動不動就丟擲異常

執行緒池使用上的建議

原理明白了,設計思想我們也明白了,程式碼要怎麼寫.光理論還不行,也就是說,我們在專案中,執行緒池究竟要怎麼用?那麼我們來看一下阿里手冊,看到這個強制相信不用我多說什麼

從執行緒池理論聊聊為什麼要看原始碼

Dubbo執行緒池

那麼我們來看看Dubbo官方文件,一直強調,官方文件才是最好的學習資料.

從執行緒池理論聊聊為什麼要看原始碼

迴歸問題

那麼回到我們前面遇到的問題.我們看了官方文件說Dubbo預設(預設)用執行緒池是fixed,那麼我們第一反應,從前面各種分析原理也得知了,FixedThreadPool的佇列是很大的,他根本達不到第三個判斷條件mamximumPoolSize,達不到第三個條件,也就不會觸發handle丟擲異常.那前面那個壓測問題的異常怎麼來的,難道肥朝上面的分析都是騙人的?肥朝也是大豬蹄子???

從執行緒池理論聊聊為什麼要看原始碼

直入原始碼

這種問題.搜尋是不好使了,因為根本不好搜尋.那麼我們只好直入原始碼了

@SPI("fixed")
public interface ThreadPool {
    
    /**
     * 執行緒池
     * 
     * @param url 執行緒引數
     * @return 執行緒池
     */
    @Adaptive({Constants.THREADPOOL_KEY})
    Executor getExecutor(URL url);

}
複製程式碼
public class FixedThreadPool implements ThreadPool {

    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, 
        		queues == 0 ? new SynchronousQueue<Runnable>() : 
        			(queues < 0 ? new LinkedBlockingQueue<Runnable>() 
        					: new LinkedBlockingQueue<Runnable>(queues)),
        		new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}
複製程式碼

此時我們發現,Dubbo裡面的FixedThreadPoolnewFixedThreadPool建立出來的FixedThreadPool引數是不一樣的.預設情況下,Dubbo的FixedThreadPool中,maximumPoolSize = 200,佇列是容量很小的SynchronousQueue.所以當執行緒超過200的時候,就會丟擲異常.這個和我們上面分析的原理是一致的.

其實換個角度想,規範手冊都是阿里出的,阿里手冊都強制說要用ThreadPoolExecutor的方式來建立了,而且還給你分析了無界佇列的風險,那麼Dubbo官方文件說的fixed又怎麼會是Executors建立的無界佇列這種呢?

知道了執行緒池的原理和異常的根源之後,我們完全可以根據業務特點的不同,自定義執行緒池引數,來避免這類異常的頻繁發生.亦或者改變Dubbo預設的執行緒模型,從aLL改成message等,這個就要結合實際的業務情況了.(這兩個方案後面有時間會把公司的真實案例抽象成簡單模型和大家分享)

寫在最後

肥朝 是一個專注於 原理、原始碼、開發技巧的技術公眾號,號內原創專題式原始碼解析、真實場景原始碼原理實戰(重點)。掃描下面二維碼關注肥朝,讓本該造火箭的你,不再擰螺絲!

從執行緒池理論聊聊為什麼要看原始碼

相關文章