關於執行緒池的面試題

weixin_34236497發表於2018-09-19

問題

問題:

  1. 單機上一個執行緒正在處理服務,如果忽然斷電了怎麼辦(正在處理和阻塞佇列裡的請求怎麼處理)
  2. 為什麼要使用執行緒池,執行緒池用什麼用
  3. 說說幾種常見的執行緒池及使用場景
  4. 執行緒池有哪幾種工作佇列
  5. 怎麼理解無界佇列和有界佇列
  6. 執行緒池中的幾種重要的引數及流程

1. 為什麼要使用執行緒池,執行緒池用什麼用

  1. 降低資源消耗:通過重用已經建立的執行緒來降低執行緒建立和銷燬的消耗
  2. 提高響應速度:任務到達時不需要等待執行緒建立就可以立即執行
  3. 提高執行緒的可管理性:執行緒池可以統一管理、分配、調優和監控

2. 說說幾種常見的執行緒池及使用場景

  • newFixedThreadPool(固定大小的執行緒池)
  • newSingleThreadExecutor(單執行緒執行緒池)
  • newCachedThreadPool(可快取執行緒的執行緒池)
  • newScheduledThreadPool

newFixedThreadPool

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

執行緒池特點:

  • 核心執行緒數和最大執行緒數大小一樣
  • keepAliveTime為0
  • 阻塞佇列是LinkedBlockingQueue

它是固定大小的執行緒池,其核心執行緒數和最大執行緒數大小一樣。並且阻塞佇列用的是LinkedBlockingQueue,也就是說執行緒最大數這個引數失效了基本,所以不會出現外包執行緒的存在,所以也可以認為keepAliveTime引數是一個擺設。除非allowCoreThreadTimeOut方法的呼叫。

該執行緒池的工作機制是:

  1. 執行緒數少於核心執行緒數,也就是設定的執行緒數時,新建執行緒執行任務
  2. 執行緒數等於核心執行緒數後,將任務加入阻塞佇列
    1. 由於佇列容量非常大(Integer.MAX_VALUE),可以一直加加加。(當執行緒池中的任務比較特殊時,比如關於資料庫的長時間的IO操作,可能導致OOM)
  3. 執行完任務的執行緒反覆去佇列中取任務執行

適用場景:

FixedThreadPool 適用於處理CPU密集型的任務,確保CPU在長期被工作執行緒使用的情況下,儘可能的少的分配執行緒即可。一般Ncpu+1

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

執行緒池特點:

  • 核心執行緒數和最大執行緒數大小一樣且都是1
  • keepAliveTime為0
  • 阻塞佇列是LinkedBlockingQueue

該執行緒池的工作機制是:

  1. 執行緒池中沒有執行緒時,新建一個執行緒執行任務
  2. 有一個執行緒以後,將任務加入阻塞佇列,不停加加加
  3. 唯一的這一個執行緒不停地去佇列裡取任務執行

適用場景:

SingleThreadExecutor適用於序列執行任務的場景,每個任務必須按順序執行,不需要併發執行。

newCachedThreadPool

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

執行緒池特點:

  • 核心執行緒數為0,且最大執行緒數為Integer.MAX_VALUE
  • 阻塞佇列是SynchronousQueue

SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue

鎖當提交任務的速度大於處理任務的速度時,每次提交一個任務,就必然會建立一個執行緒。極端情況下會建立過多的執行緒,耗盡 CPU 和記憶體資源。由於空閒 60 秒的執行緒會被終止,長時間保持空閒的 CachedThreadPool 不會佔用任何資源。

該執行緒池的工作機制是:

  1. 沒有核心執行緒,直接向SynchronousQueue中提交任務
  2. 如果有空閒執行緒,就去取出任務執行;如果沒有空閒執行緒,就新建一個
  3. 執行完任務的執行緒有60秒生存時間,如果在這個時間內可以接到新任務,就可以繼續活下去,否則就拜拜

適用場景:

CachedThreadPool 用於併發執行大量短期的小任務。

newScheduledThreadPool

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


public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

執行緒池特點:

  • 最大執行緒數為Integer.MAX_VALUE
  • 阻塞佇列是DelayedWorkQueue

ScheduledThreadPoolExecutor 新增任務提供了另外兩個方法:

  • scheduleAtFixedRate() :按某種速率週期執行
  • scheduleWithFixedDelay():在某個延遲後執行

兩種方法的內部實現都是建立了一個ScheduledFutureTask物件封裝了任務的延遲執行時間及執行週期,並呼叫decorateTask()方法轉成RunnableScheduledFuture物件,然後新增到延遲佇列中。

DelayQueue:中封裝了一個優先順序佇列,這個佇列會對佇列中的ScheduledFutureTask 進行排序,兩個任務的執行 time 不同時,time 小的先執行;否則比較新增到佇列中的ScheduledFutureTask的順序號 sequenceNumber ,先提交的先執行。

該執行緒池的工作機制是:

  1. 呼叫上面兩個方法新增一個任務
  2. 執行緒池中的執行緒從 DelayQueue 中取任務
  3. 然後執行任務

具體執行步驟:

  1. 執行緒從 DelayQueue 中獲取 time 大於等於當前時間的 ScheduledFutureTask
    1. DelayQueue.take()
  2. 執行完後修改這個 task 的 time 為下次被執行的時間
  3. 然後再把這個 task 放回佇列中
    1. DelayQueue.add()

適用場景:

ScheduledThreadPoolExecutor用於需要多個後臺執行緒執行週期任務,同時需要限制執行緒數量的場景。

3. 執行緒池有哪幾種工作佇列

  • ArrayBlockingQueue (有界佇列):是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序。
  • LinkedBlockingQueue (無界佇列):一個基於連結串列結構的阻塞佇列,此佇列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。
  • SynchronousQueue(同步佇列): 一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。
  • DelayQueue(延遲佇列):一個任務定時週期的延遲執行的佇列。根據指定的執行時間從小到大排序,否則根據插入到佇列的先後排序。
  • PriorityBlockingQueue(優先順序佇列): 一個具有優先順序得無限阻塞佇列。

4. 怎麼理解無界佇列和有界佇列

  • 有界佇列即長度有限,滿了以後ArrayBlockingQueue會插入阻塞。
  • 無界佇列就是裡面能放無數的東西而不會因為佇列長度限制被阻塞,但是可能會出現OOM異常。

5. 執行緒池中的幾種重要的引數及流程

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize:核心池的大小,在建立了執行緒池後,預設情況下,執行緒池中並沒有任何執行緒,而是等待有任務到來才建立執行緒去執行任務,當有任務來之後,就會建立一個執行緒去執行任務,當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到快取佇列當中
  • maximumPoolSize:執行緒池最大執行緒數最大執行緒數
  • keepAliveTime:表示執行緒沒有任務執行時最多保持多久時間會終止
  • unit:引數keepAliveTime的時間單位TimeUtil類的列舉類(DAYS、HOURS、MINUTES、SECONDS 等)
  • workQueue:阻塞佇列,用來儲存等待執行的任務
  • threadFactory:執行緒工廠,主要用來建立執行緒
  • handler:拒絕處理任務的策略
    • AbortPolicy:丟棄任務並丟擲 RejectedExecutionException 異常。(預設這種)
    • DiscardPolicy:也是丟棄任務,但是不丟擲異常
    • DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
    • CallerRunsPolicy:由呼叫執行緒處理該任務
2111160-dbda6af25320bdad.jpg
image

執行流程

  1. 當有任務進入時,執行緒池建立執行緒去執行任務,直到核心執行緒數滿為止
  2. 核心執行緒數量滿了之後,任務就會進入一個緩衝的任務佇列中
    1. 當任務佇列為無界佇列時,任務就會一直放入緩衝的任務佇列中,不會和最大執行緒數量進行比較
    2. 當任務佇列為有界佇列時,任務先放入緩衝的任務佇列中,當任務佇列滿了之後,才會將任務放入執行緒池,此時會拿當前執行緒數與執行緒池允許的最大執行緒數進行比較,如果超出了,則預設會丟擲異常。如果沒超出,然後執行緒池才會建立執行緒並執行任務,當任務執行完,又會將緩衝佇列中的任務放入執行緒池中,然後重複此操作。

6. 單機上一個執行緒正在處理服務,如果忽然斷電了怎麼辦(正在處理和阻塞佇列裡的請求怎麼處理)

經過網上查閱,發現基本是沒有一個明確的回答的。不過思考過後一番,我感覺實現思路和MySQL的redo,undo功能很相似,我們可以對正在處理和阻塞佇列的任務做事物管理或者對阻塞佇列中的任務持久化處理,並且當斷電或者系統崩潰,操作無法繼續下去的時候,可以通過回溯日誌的方式來撤銷正在處理的已經執行成功的操作。然後重新執行整個阻塞佇列。

即:

阻塞佇列持久化,正在處理事物控制。斷電之後正在處理的回滾,日誌恢復該次操作。伺服器重啟後阻塞佇列中的資料再載入

參考

相關文章