執行緒池的阻塞佇列的理解

擊水三千里發表於2019-03-05

常見的三種工廠類執行緒池

1、newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

 由於佇列緩衝區為空,每來一個任務時,都會在必要時新建執行緒執行任務直到到達Integer.MAX_VALUE,這就有可能導致大量的執行緒被建立,進而系統癱瘓

2、newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        //超過nThreads個執行緒後會無限制的往阻塞佇列放入執行緒
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

 

3、newSingleThreadExecutor 

建立一個單執行緒化的執行緒池

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
         //超過1個執行緒後會無限制的往阻塞佇列放入執行緒
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

 4、newScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }


執行緒池常用阻塞佇列

1.SynchronousQueue

private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(4, Runtime.getRuntime().availableProcessors() * 2, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), r -> new Thread(r, "ThreadTest"));

SynchronousQueue沒有容量,是無緩衝等待佇列,是一個不儲存元素的阻塞佇列,會直接將任務交給消費者,必須等佇列中的新增元素被消費後才能繼續新增新的元素。

擁有公平(FIFO)和非公平(LIFO)策略,非公平側羅會導致一些資料永遠無法被消費的情況

使用SynchronousQueue阻塞佇列一般用於構造newCachedThreadPool


2.LinkedBlockingQueue

private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(4, Runtime.getRuntime().availableProcessors() * 2, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, "ThreadTest"));

LinkedBlockingQueue是一個無界快取等待佇列。當前執行的執行緒數量達到corePoolSize的數量時,剩餘的元素會在阻塞佇列裡等待。(所以在使用此阻塞佇列時maximumPoolSizes就相當於無效了),每個執行緒完全獨立於其他執行緒。生產者和消費者使用獨立的鎖來控制資料的同步,即在高併發的情況下可以並行操作佇列中的資料。


3.ArrayBlockingQueue

 private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(4, Runtime.getRuntime().availableProcessors() * 2, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(32), r -> new Thread(r, "ThreadTest"));

ArrayBlockingQueue是一個有界快取等待佇列,可以指定快取佇列的大小,當正在執行的執行緒數等於corePoolSize時,多餘的元素快取在ArrayBlockingQueue佇列中等待有空閒的執行緒時繼續執行,當ArrayBlockingQueue已滿時,加入ArrayBlockingQueue失敗,會開啟新的執行緒去執行,當執行緒數已經達到最大的maximumPoolSizes時,再有新的元素嘗試加入ArrayBlockingQueue時會報錯。

 

執行緒池拒絕策略

1、CallerRunsPolicy:執行緒呼叫執行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。 
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); }} 
這個策略顯然不想放棄執行任務。但是由於池中已經沒有任何資源了,那麼就直接使用呼叫該execute的執行緒本身來執行。(開始我總不想丟棄任務的執行,但是對某些應用場景來講,
很有可能造成當前執行緒也被阻塞。如果所有執行緒都是不能執行的,很可能導致程式沒法繼續跑了。需要視業務情景而定吧。)

2、AbortPolicy:處理程式遭到拒絕將丟擲執行時 RejectedExecutionException 
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {throw new RejectedExecutionException();} 
這種策略直接丟擲異常,丟棄任務。(jdk預設策略,佇列滿併執行緒滿時直接拒絕新增新任務,並丟擲異常,所以說有時候放棄也是一種勇氣,為了保證後續任務的正常進行,丟棄一些也是可以接收的,記得做好記錄)

3、DiscardPolicy:不能執行的任務將被刪除 
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {} 這種策略和AbortPolicy幾乎一樣,也是丟棄任務,只不過他不丟擲異常。

4、DiscardOldestPolicy:如果執行程式尚未關閉,則位於工作佇列頭部的任務將被刪除,然後重試執行程式(如果再次失敗,則重複此過程) 
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) {e.getQueue().poll();e.execute(r); }} 
該策略就稍微複雜一些,在pool沒有關閉的前提下首先丟掉快取在隊

 

如何合理設定執行緒池佇列長度

tomcat、Dubbo 等業界成熟的產品是如何設定執行緒佇列,我們主要分析下這兩個中介軟體

1.JDK執行緒池策略

1. 如果此時執行緒池中的數量小於corePoolSize,即使執行緒池中的執行緒都處於空閒狀態,也要建立新的執行緒來處理被新增的任務。
2. 如果此時執行緒池中的數量大於等於corePoolSize,但是緩衝佇列 workQueue未滿,那麼任務被放入緩衝佇列
3. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量小於maximumPoolSize,建新的執行緒來處理新增的任務。
4. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量等於maximumPoolSize,那麼通過 handler所指定的策略來處理此任務。也就是處理任務的優先順序為:核心執行緒corePoolSize、任務佇列workQueue、最大執行緒maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
5. 當執行緒池中的執行緒數量大於corePoolSize時,如果某執行緒空閒時間超過keepAliveTime,執行緒將被終止。這樣,執行緒池可以動態的調整池中的執行緒數。

2.Tomcat執行緒池策略

Tomcat的執行緒池佇列是無限長度的,但是執行緒池會一直建立到maximumPoolSize,然後才把請求放入等待佇列中

tomcat 任務佇列org.apache.tomcat.util.threads.TaskQueue其繼承與LinkedBlockingQueue,覆寫offer方法。 

    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
         //執行緒個數小於MaximumPoolSize會建立新的執行緒。
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

3.Dubbo執行緒池策略

        Dubbo 提供3種執行緒池模型即:FixedThreadPool、CachedThreadPool(客戶端預設的)、LimitedThreadPool(服務端預設的),從原始碼可以看出,其預設的佇列長度都是0,當佇列長度為0 ,其使用是無緩衝的佇列SynchronousQueue,當執行執行緒超過maximumPoolSize則拒絕請求。 

總結

 執行緒池的任務佇列本來起緩衝作用,但是如果設定的不合理會導致執行緒池無法擴容至max,這樣無法發揮多執行緒的能力,導致一些服務響應變慢。

       佇列長度要看具體使用場景,取決服務端處理能力以及客戶端能容忍的超時時間等

       建議採用tomcat的處理方式,core與max一致,先擴容到max再放佇列,不過佇列長度要根據使用場景設定一個上限值,如果響應時間要求較高的系統可以設定為0。

相關文章