完美避開線程池血坑

江湖草根測試小A經過三年蟄伏,聲名鵲起,終於鼓起勇氣,去參與了自己嚮往已久的霸主阿里的選拔。經過一番精心準備,雄心萬丈的小A來到阿里參加了入門考察,結果遭遇當頭一板磚(FixedThreadPool在實戰中是如何運用的),直接被淘汰。無奈之餘,小A只能灰溜溜的回到門派,並虛心向師傅資深測試大C請教。大C醞釀了一下,完整的解釋了一下線程池,並重點介紹了一下FixedThreadPool及其使用場景。聽完了大C的介紹,小A只能感嘆個人還需提高。讓我們追隨大C的講解,一起提升一下自己吧。

一、線程池的定義

一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。可用線程數量應該取決於可用的併發處理器、處理器內核、內存、網絡sockets等的數量。例如,線程數一般取cpu數量+2比較合適,線程數過多會導致額外的線程切換開銷。

二、線程池的創建類型

線程池4種默認類型如下:

1、newCachedThreadPool

創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。用法如下:

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(index);
            }
        });
    }
    cachedThreadPool.shutdown();

線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。

缺點:一般不用,是因爲newCachedThreadPool 可以無限的新建線程,容易造成堆外內存溢出,因爲它的最大值是在初始化的時候設置爲 Integer.MAX_VALUE,一般來說機器都沒那麼大內存給它不斷使用。當然知道可能出問題的點,就可以去重寫一個方法限制一下這個最大值。

2、newFixedThreadPool

Executors.newFixedThreadPool(n)創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

下面代碼創建了5個線程,但是定長線程池的大小最好根據系統資源進行設置,如Runtime.getRuntime().availableProcessors(),可參考PreloadDataCache。其實newFixedThreadPool()在嚴格上說並不會複用線程,每運行一個Runnable都會通過ThreadFactory創建一個線程。

ExecutorService cachedThreadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
        final int index = i;
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(index);
            }
        });
    }
    cachedThreadPool.shutdown();
3、newScheduledThreadPool

創建一個定長線程池,支持定時及週期性任務執行。下面定義線程池,最大線程數是5。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

延遲執行:

 scheduledThreadPool.schedule(new Runnable() {@Override
        public void run()
 {
            System.out.println("delay");
        }
    }, 3, TimeUnit.SECONDS);

延遲1秒,並每隔3秒定期執行:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {@Override
        public void run()
 {
            System.out.println("delay 1 seconds, and excute every 3 seconds");
        }
    }, 1, 3, TimeUnit.SECONDS);

關於延遲執行和週期性執行我們還會想到Timer

Timer timer = new Timer();
    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            
        }
    };
    timer.schedule(timerTask, 1000, 3000);

Timer 的優點在於簡單易用,但由於所有任務都是由同一個線程來調度,因此所有任務都是串行執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到之後的任務(比如:一個任務出錯,以後的任務都無法繼續)。

ScheduledThreadPoolExecutor的設計思想是,每一個被調度的任務都會由線程池中一個線程去執行,因此任務是併發執行的,相互之間不會受到干擾。需要注意的是,只有當任務的執行時間到來時,ScheduedExecutor 纔會真正啓動一個線程,其餘時間 ScheduledExecutor 都是在輪詢任務的狀態。

通過對比可以發現ScheduledExecutorService比Timer更安全,功能更強大,在以後的開發中儘可能使用ScheduledExecutorService(JDK1.5以後)替代Timer。

4、newSingleThreadExecutor

Executors.newSingleThreadExecutor(創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        singleThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

三、線程的啓動方式

Java可以用以下三種方式來創建線程

  1. 繼承Thread類創建線程
  2. 實現Runnable接口創建線程
  3. 使用Callable和Future創建線程

四、FixedThreadPool原理

FixedTreadPool在實際開發過程中是不建議使用的,同時阿里巴巴Java開發手冊中也明確指出,而且用的詞是『不允許』使用Executors創建定長線程池。因爲可能會導致OOM(OutOfMemory ,內存溢出),但是並沒有說明爲什麼,那麼接下來就從源碼來看一下到底爲什麼不允許?

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

在定義newFixedThreadPool時,會創建一個LinkedBlockingQueue,但是未指定容量。此時,LinkedBlockingQueue就是一個無邊界隊列,對於一個無邊界隊列來說,是可以不斷的向隊列中加入任務的,這種情況下就有可能因爲任務過多而導致內存溢出問題。(Java中的LinkedBlockingQueue是一個用鏈表實現的有界阻塞隊列,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度爲Integer.MAX_VALUE)。既然存在內存溢出的問題,顯然在實際項目開發中,newFixedThreadPool應該是實際摒棄的,那正確的使用姿勢是什麼呢?

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
  60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));

主要是避免使用其中的默認實現,直接調用ThreadPoolExecutor的構造函數來自己創建線程池。在創建的同時,給BlockQueue指定容量就可以了。

以上介紹了線程池的定義及其4種默認實現,並從源碼角度上分析了FixedTreadPool可能存在的問題,顯然小A在這裏是被阿里大俠給挖了一個坑埋了,失敗自然就不可避免。

部分測試同學可能會質疑,我們需要了解這些嗎?答案顯而易見,首先這個是進入BAT大廠的敲門磚,其次如果知道了其中的優劣及適用場景,在白盒測試過程中可以快速發現系統實現可能存在的問題,並且在平臺工具開發過程中,高併發是一個繞不開的門檻,選擇合適的框架可以使我們自己開發的平臺工具對我們的實際業務測試切實提效,傳統的點點點測試方式終將會被逐步被淘汰。讓我們一起練好內功,迎接時代的變化吧。

其他文章可以關注微信公衆號測試架構師養成記,還有資料可以領哦~
在這裏插入圖片描述

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