[肥朝]從線程池理論聊聊爲何要看源碼

前言

不少時候,我都想向你們傳輸一個思想,那就是隻有懂了原理,才能隨心爲所欲爲寫代碼.而看源碼,又是瞭解原理的一個很是重要的途徑.java

然而,肥朝以前的文章,大體分爲三類git

第三點,我認爲尤爲重要.咱們看源碼的目的是爲了解決問題,我以爲只談付出,不談回報都是耍流氓.若是隻告訴你們要懂原理,看源碼,接着貼幾大段源碼,而後給大片大片的源碼打上註釋.看了大段大段的註釋下來,好像都懂了,感受很"充實".性能

可是咱們要的並非這種自我感受的"充實",而是真真正正經過源碼,解決了 搜索沒法解決的問題 ,只有這樣.纔是有收穫的.若是百度隨便一搜都有答案的那你還捨近求遠的看源碼這就實在是裝逼了學習

直入主題

今天在公司壓測的性能羣,出現了這麼一個問題,以下圖:ui

粗略一看,大概Dubbo線程池達到最大線程數拋出的異常.那麼咱們先來鋪墊線程池的知識基本儲備url

常見線程池

  • SingleThreadExecutor: 單線程線程池,通常不多使用.spa

  • 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等,這個就要結合實際的業務狀況了.(這兩個方案後面有時間會把公司的真實案例抽象成簡單模型和你們分享)

寫在最後

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

相關文章
相關標籤/搜索