面試-線程池原理

前言

線程池有哪些

使用過的話,這個應該可以答出來,固定大小的線程池 newFixedThreadPool,單線程線程池 newSingleThreadExecutor,無限大小的線程池 newCachedThreadPool ,和週期性執行線程池 newScheduledThreadPool

它們都是構造了一個 ThreadPoolExecutor 實例,使用了不同的參數而已,可能會問你使用場景,這時你可以這樣甩它一臉,一般都不會用 JDK 定義好的,因爲有坑,而是自己使用參數構建一個 ThreadPoolExecutor 實例。

線程池的執行流程

基本面試必問,可能會以另一種方式問你有幾個核心參數或者讓你設計一個線程池你會如何設計。

線程池有這樣一些參數 ,基本上池相關的,都會有這些,像 tomcat 的連接池,數據庫連接池,redis 連接池,http 連接池。

  • 核心線程數 corePoolSize,多出的線程會被銷燬
  • 最大線程數 maximunPoolSize,當處在高峯期時所能創建的最大線程數
  • 非核心線程保留時間 keepAliveTime, 非核心線程能夠空閒的最大時間,如超過時間,則銷燬
  • 緩存隊列 workQueue,當創建的線程數超過核心線程數時,線程會進入等待隊列等待,直到隊滿

執行流程如下,在 ThreadPoolExecutor.execute 有詳細英語說明

  1. 在當前池中線程數還少於核心線程數時,會先創建線程,把當前任務做爲這個線程的第一個任務來執行
  2. 當池中線程數超過核心線程數後,會先存儲任務到隊列,並且檢查當前池中線程數,因爲可能有掛掉的線程需要補充
  3. 如果隊列也加不進線程了,嘗試添加新的線程,直到最大線程數,如果撐不住了,執行拒絕策略
  4. 過了高峯期後,如果有設置 keepAliveTime ,會把線程數量降到核心線程數

可能會問 execute 和 submit 有什麼區別

submit 其實就是調用了 execute ,submit 可以有返回值 Future 用於異步返回

可以使用的緩存隊列有哪些,有什麼特點

LinkedBlockingQueue 基於鏈表實現的隊列,如果不指定容量,默認就爲 Integer.MAX_VALUE,它是 newFixedThreadPool 和 newSingleThreadExecutor 默認使用的隊列,當在高峯期時,會把內存撐爆

ArrayBlockingQueue 基於數組實現的隊列,和 LinkedBlockingQueue 類似

SynchronousQueue 沒有容量,必須要等添加的數據被消費後才能繼續添加元素,用在 newCachedThreadPool 線程池中,它的最大線程數爲Integer.MAX_VALUE 所以這種線程池在高峯時會不斷的創建線程,直到不能創建線程耗盡資源

DelayedWorkQueue 使用堆實現的優先級隊列,會不斷增長空間,最大線程數也是 Integer.MAX_VALUE ,但定時任務不會有太高的併發,不過定時任務一般不使用這個,而是一些 quartz 任務框架,可以將任務持久 ,如果是在多實例部署的情況下,還應該使用分佈式任務 xxljob 或者 elasticjob

拒絕策略

拒絕策略用於當隊列滿了,並且線程數了已經到達最大線程數時,執行的策略。

Jdk 內置拒絕策略 有拋棄當前任務,拋棄舊任務,異常,運行在提交任務的線程,默認是異常,除了運行在提交任務線程外,其它的都會拒絕當前任務,運行在提交任務線程也會阻塞下一個任務的提交,所以這個拒絕策略經常也需要重寫才能達到要求。

第三方的拒絕策略有 dubbo 中的也是異常,但會打印出更加詳細的信息出來 , netty 會新起線程來運行任務,activeMq 會等一分鐘後重新放入隊列,pinpoint 使用責任鏈模式執行一個拒絕鏈。

以上拒絕策略詳情見博客說明 http://www.kailing.pub/article/index/arcid/255.html

我之前也寫過一種拒絕策略,使用斐波拉契數列做爲等待時間,初始參數爲 100ms,100ms,每次等待一段時間後重新提交任務, 在等待時間超過 3 分鐘後還沒有執行任務,拋出異常。

/**
     * 線程池拒絕策略,等待一會(<斐波拉契數列:初始值爲 100,100>),直到線程池空閒,然後再次提交線程;
     * 當線程池空閒時間過長(1 min)後,初始化等待時間
     * 當線程池過載,等待時間過長 (3 min) 後,拋出異常
     */
class DefaultRejectedExecutionHandler implements RejectedExecutionHandler {
    private Log logger = LogFactory.getLog(DefaultRejectedExecutionHandler.class);

    // 上次等待時間
    private long lastWaitTime = 100;
    private long lastLastWaitTime = 100;
    private long maxWaitTime = DateUtils.MILLIS_PER_MINUTE * 3 ;
    private long maxIdleTime = DateUtils.MILLIS_PER_MINUTE;

    //最後一次池子不空閒時間
    private long lastPoolUnIdleTime = System.currentTimeMillis();

    @Override
    public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
        if(System.currentTimeMillis() - lastPoolUnIdleTime >= maxIdleTime){
            //如果池子空閒時間超過最大值,下次等待時就初始化爲最開始等待時間
            lastWaitTime = 100;
            lastLastWaitTime = 100;
        }

        if(lastWaitTime >= maxWaitTime){
            //如果上次等待時間超過 3 分鐘 ,則需要增大線程池大小,並需要查詢什麼操作耗時過長
            logger.error("上次等待時間["+lastWaitTime+"]超過最大等待時間["+maxWaitTime+"] ms,當前請求獲取線程被拒絕;當前線程池配置 ==> \n" +
                         " 最大池大小["+maxPoolSize+"],核心維護大小["+corePoolSize+"],排隊數["+queueCapacity+"]");
            return ;
        }

        //當前池子不是空閒的,記錄池子最後不是空閒的時間
        lastPoolUnIdleTime = System.currentTimeMillis();

        //如果等待時間未超過最大等待時間,則以 <斐波拉契數列> 獲取等待時間 200,300,500,800,1300...
        long nowWaitTime = lastWaitTime + lastLastWaitTime ;

        try {
            //否則等待一段時間後重新提交線程
            Thread.sleep(nowWaitTime);
            executor.submit(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


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