從線程池理論淺析爲什麼要看源碼

前言

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

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

  • 源碼解析,穿插怎麼看源碼(參考肥朝Dubbo源碼解析系列文章)

  • 怎麼臨摹一個一比一的源碼(參考肥朝dubbo源碼解析-簡單原理、與spring融合、dubbo源碼解析-spi(五))

  • 看了源碼,究竟解決了什麼問題(參考肥朝還有這種操作?淺析爲什麼要看源碼)

第三點,我認爲尤其重要.我們看源碼的目的是爲了解決問題,我覺得只談付出,不談回報都是耍流氓.如果只告訴大家要懂原理,看源碼,接着貼幾大段源碼,然後給大片大片的源碼打上註釋.看了大段大段的註釋下來,好像都懂了,感覺很"充實".但是我們要的並不是這種自我感覺的"充實",而是真真正正通過源碼,解決了搜索無法解決的問題,只有這樣.纔是有收穫的.如果百度隨便一搜都有答案的那你還捨近求遠的看源碼這就實在是裝逼了

直入主題

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

粗略一看,大概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官方文檔,一直強調,官方文檔纔是最好的學習資料.

img

迴歸問題

那麼回到我們前面遇到的問題.我們看了官方文檔說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創建的×××隊列這種呢?

知道了線程池的原理和異常的根源之後,我們完全可以根據業務特點的不同,自定義線程池參數,來避免這類異常的頻繁發生.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章