當面試官問執行緒池時,你應該知道些什麼?

李紅歐巴發表於2019-04-11

概述

什麼是執行緒池?

執行緒池是一種多執行緒處理形式,處理過程中將任務新增到佇列,然後在建立執行緒後自動啟動這些任務。

為什麼要用執行緒池?

  • 降低資源消耗
    • 通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度
    • 當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性
    • 執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。但是要做到合理的利用執行緒池,必須對其原理了如指掌。

Executor 框架

簡介

semaphore

  • Executor:一個介面,其定義了一個接收 Runnable 物件的方法 executor,其方法簽名為 executor(Runnable command),
  • ExecutorService:是一個比 Executor 使用更廣泛的子類介面,其提供了生命週期管理的方法,以及可跟蹤一個或多個非同步任務執行狀況返回 Future 的方法。
  • AbstractExecutorService:ExecutorService 執行方法的預設實現。
  • ScheduledExecutorService:一個可定時排程任務的介面。
  • ScheduledThreadPoolExecutor:ScheduledExecutorService 的實現,一個可定時排程任務的執行緒池。
  • ThreadPoolExecutor:執行緒池,可以通過呼叫 Executors 以下靜態工廠方法來建立執行緒池並返回一個 ExecutorService 物件。

ThreadPoolExecutor

java.uitl.concurrent.ThreadPoolExecutor 類是 Executor 框架中最核心的一個類。

ThreadPoolExecutor 有四個構造方法,前三個都是基於第四個實現。第四個構造方法定義如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {複製程式碼

引數說明

  • corePoolSize:執行緒池的基本執行緒數。這個引數跟後面講述的執行緒池的實現原理有非常大的關係。在建立了執行緒池後,預設情況下,執行緒池中並沒有任何執行緒,而是等待有任務到來才建立執行緒去執行任務,除非呼叫了 prestartAllCoreThreads()或者 prestartCoreThread()方法,從這 2 個方法的名字就可以看出,是預建立執行緒的意思,即在沒有任務到來之前就建立 corePoolSize 個執行緒或者一個執行緒。預設情況下,在建立了執行緒池後,執行緒池中的執行緒數為 0,當有任務來之後,就會建立一個執行緒去執行任務,當執行緒池中的執行緒數目達到 corePoolSize 後,就會把到達的任務放到快取佇列當中。
  • maximumPoolSize:執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是如果使用了無界的任務佇列這個引數就沒什麼效果。
  • keepAliveTime:執行緒活動保持時間。執行緒池的工作執行緒空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高執行緒的利用率。
  • unit:引數 keepAliveTime 的時間單位,有 7 種取值。可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue:任務佇列。用於儲存等待執行的任務的阻塞佇列。 可以選擇以下幾個阻塞佇列。
    • ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序。
    • LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按 FIFO (先進先出) 排序元素,吞吐量通常要高於 ArrayBlockingQueue。靜態工廠方法 Executors.newFixedThreadPool()使用了這個佇列。
    • SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於 LinkedBlockingQueue,靜態工廠方法 Executors.newCachedThreadPool 使用了這個佇列。
    • PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。
  • threadFactory:建立執行緒的工廠。可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字。
  • handler:飽和策略。當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是 AbortPolicy,表示無法處理新任務時丟擲異常。以下是 JDK1.5 提供的四種策略。
    • AbortPolicy:直接丟擲異常。
    • CallerRunsPolicy:只用呼叫者所線上程來執行任務。
    • DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
    • DiscardPolicy:不處理,丟棄掉。
    • 當然也可以根據應用場景需要來實現 RejectedExecutionHandler 介面自定義策略。如記錄日誌或持久化不能處理的任務。

重要方法

在 ThreadPoolExecutor 類中有幾個非常重要的方法:

  • execute() 方法實際上是 Executor 中宣告的方法,在 ThreadPoolExecutor 進行了具體的實現,這個方法是 ThreadPoolExecutor 的核心方法,通過這個方法可以向執行緒池提交一個任務,交由執行緒池去執行。
  • submit() 方法是在 ExecutorService 中宣告的方法,在 AbstractExecutorService 就已經有了具體的實現,在 ThreadPoolExecutor 中並沒有對其進行重寫,這個方法也是用來向執行緒池提交任務的,但是它和 execute()方法不同,它能夠返回任務執行的結果,去看 submit()方法的實現,會發現它實際上還是呼叫的 execute()方法,只不過它利用了 Future 來獲取任務執行結果(Future 相關內容將在下一篇講述)。
  • shutdown()shutdownNow() 是用來關閉執行緒池的。

向執行緒池提交任務

我們可以使用 execute 提交任務,但是 execute 方法沒有返回值,所以無法判斷任務是否被執行緒池執行成功。

通過以下程式碼可知 execute 方法輸入的任務是一個 Runnable 例項。

threadsPool.execute(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
            }
        });複製程式碼

我們也可以使用 submit 方法來提交任務,它會返回一個 Future ,那麼我們可以通過這個 Future 來判斷任務是否執行成功。

通過 Futureget 方法來獲取返回值,get 方法會阻塞住直到任務完成。而使用 get(long timeout, TimeUnit unit) 方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。

Future<Object> future = executor.submit(harReturnValuetask);
try {
     Object s = future.get();
} catch (InterruptedException e) {
    // 處理中斷異常
} catch (ExecutionException e) {
    // 處理無法執行任務異常
} finally {
    // 關閉執行緒池
    executor.shutdown();
}複製程式碼

執行緒池的關閉

我們可以通過呼叫執行緒池的 shutdownshutdownNow 方法來關閉執行緒池,它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的 interrupt 方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow 首先將執行緒池的狀態設定成 STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而 shutdown 只是將執行緒池的狀態設定成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法的其中一個,isShutdown 方法就會返回 true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫 isTerminaed 方法會返回 true。至於我們應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫 shutdown 來關閉執行緒池,如果任務不一定要執行完,則可以呼叫 shutdownNow。

Executors

JDK 中提供了幾種具有代表性的執行緒池,這些執行緒池是基於 ThreadPoolExecutor 的定製化實現。

在實際使用執行緒池的場景中,我們往往不是直接使用 ThreadPoolExecutor ,而是使用 JDK 中提供的具有代表性的執行緒池例項。

newCachedThreadPool

建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。

這種型別的執行緒池特點是:

  • 工作執行緒的建立數量幾乎沒有限制(其實也有限制的,數目為 Interger.MAX_VALUE), 這樣可靈活的往執行緒池中新增執行緒。
  • 如果長時間沒有往執行緒池中提交任務,即如果工作執行緒空閒了指定的時間(預設為 1 分鐘),則該工作執行緒將自動終止。終止後,如果你又提交了新的任務,則執行緒池重新建立一個工作執行緒。
  • 在使用 CachedThreadPool 時,一定要注意控制任務的數量,否則,由於大量執行緒同時執行,很有會造成系統癱瘓。

示例:

public class CachedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(index * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " 執行,i = " + index));
        }
    }
}複製程式碼

newFixedThreadPool

建立一個指定工作執行緒數量的執行緒池。每當提交一個任務就建立一個工作執行緒,如果工作執行緒數量達到執行緒池初始的最大數,則將提交的任務存入到池佇列中。

FixedThreadPool 是一個典型且優秀的執行緒池,它具有執行緒池提高程式效率和節省建立執行緒時所耗的開銷的優點。但是,線上程池空閒時,即執行緒池中沒有可執行任務時,它不會釋放工作執行緒,還會佔用一定的系統資源。

示例:

public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            executorService.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 執行,i = " + index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}複製程式碼

newSingleThreadExecutor

建立一個單執行緒化的 Executor,即只建立唯一的工作者執行緒來執行任務,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。如果這個執行緒異常結束,會有另一個取代它,保證順序執行。單工作執行緒最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個執行緒是活動的。

示例:

public class SingleThreadExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            executorService.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 執行,i = " + index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}複製程式碼

newScheduleThreadPool

建立一個執行緒池,可以安排任務在給定延遲後執行,或定期執行。

public class ScheduledThreadPoolDemo {

    private static void delay() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + " 延遲 3 秒"), 3,
                TimeUnit.SECONDS);
    }

    private static void cycle() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        scheduledThreadPool.scheduleAtFixedRate(
                () -> System.out.println(Thread.currentThread().getName() + " 延遲 1 秒,每 3 秒執行一次"), 1, 3,
                TimeUnit.SECONDS);
    }

    public static void main(String[] args) {
        delay();
        cycle();
    }
}複製程式碼

原始碼

執行緒池的具體實現原理,大致從以下幾個方面講解:

  1. 執行緒池狀態
  2. 任務的執行
  3. 執行緒池中的執行緒初始化
  4. 任務快取佇列及排隊策略
  5. 任務拒絕策略
  6. 執行緒池的關閉
  7. 執行緒池容量的動態調整

執行緒池狀態

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }複製程式碼

runState 表示當前執行緒池的狀態,它是一個 volatile 變數用來保證執行緒之間的可見性;

下面的幾個 static final 變數表示 runState 可能的幾個取值。

當建立執行緒池後,初始時,執行緒池處於 RUNNING 狀態;

RUNNING -> SHUTDOWN

如果呼叫了 shutdown()方法,則執行緒池處於 SHUTDOWN 狀態,此時執行緒池不能夠接受新的任務,它會等待所有任務執行完畢。

(RUNNING or SHUTDOWN) -> STOP

如果呼叫了 shutdownNow()方法,則執行緒池處於 STOP 狀態,此時執行緒池不能接受新的任務,並且會去嘗試終止正在執行的任務。

SHUTDOWN -> TIDYING

當執行緒池和佇列都為空時,則執行緒池處於 TIDYING 狀態。

STOP -> TIDYING

當執行緒池為空時,則執行緒池處於 TIDYING 狀態。

TIDYING -> TERMINATED

當 terminated() 回撥方法完成時,執行緒池處於 TERMINATED 狀態。

任務的執行

任務執行的核心方法是 execute() 方法。執行步驟如下:

  1. 如果少於 corePoolSize 個執行緒正在執行,嘗試使用給定命令作為第一個任務啟動一個新執行緒。對 addWorker 的呼叫會自動檢查 runState 和 workerCount,從而防止在不應該的情況下新增執行緒。
  2. 如果任務排隊成功,仍然需要仔細檢查是否應該新增一個執行緒(因為現有的執行緒自上次檢查以來已經死亡)或者自從進入方法後,執行緒池就關閉了。所以我們重新檢查狀態,如果有必要的話,線上程池停止狀態時回滾佇列,如果沒有執行緒的話,就開始一個新的執行緒。
  3. 如果任務排隊失敗,那麼我們嘗試新增一個新的執行緒。如果失敗了,說明執行緒池已經關閉了,或者已經飽和了,所以拒絕這個任務。
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}複製程式碼

執行緒池中的執行緒初始化

預設情況下,建立執行緒池之後,執行緒池中是沒有執行緒的,需要提交任務之後才會建立執行緒。

在實際中如果需要執行緒池建立之後立即建立執行緒,可以通過以下兩個方法辦到:

prestartCoreThread():初始化一個核心執行緒; prestartAllCoreThreads():初始化所有核心執行緒

public boolean prestartCoreThread() {
    return addIfUnderCorePoolSize(null); //注意傳進去的引數是null
}

public int prestartAllCoreThreads() {
    int n = 0;
    while (addIfUnderCorePoolSize(null))//注意傳進去的引數是null
        ++n;
    return n;
}複製程式碼

任務快取佇列及排隊策略

在前面我們多次提到了任務快取佇列,即 workQueue,它用來存放等待執行的任務。

workQueue 的型別為 BlockingQueue,通常可以取下面三種型別:

  1. ArrayBlockingQueue:基於陣列的先進先出佇列,此佇列建立時必須指定大小;
  2. LinkedBlockingQueue:基於連結串列的先進先出佇列,如果建立時沒有指定此佇列大小,則預設為 Integer.MAX_VALUE;
  3. SynchronousQueue:這個佇列比較特殊,它不會儲存提交的任務,而是將直接新建一個執行緒來執行新來的任務。

任務拒絕策略

當執行緒池的任務快取佇列已滿並且執行緒池中的執行緒數目達到 maximumPoolSize,如果還有任務到來就會採取任務拒絕策略,通常有以下四種策略

  • ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲 RejectedExecutionException 異常。
  • ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
  • ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務

執行緒池的關閉

ThreadPoolExecutor 提供了兩個方法,用於執行緒池的關閉,分別是 shutdown()和 shutdownNow(),其中:

  • shutdown():不會立即終止執行緒池,而是要等所有任務快取佇列中的任務都執行完後才終止,但再也不會接受新的任務
  • shutdownNow():立即終止執行緒池,並嘗試打斷正在執行的任務,並且清空任務快取佇列,返回尚未執行的任務

執行緒池容量的動態調整

ThreadPoolExecutor 提供了動態調整執行緒池容量大小的方法:setCorePoolSize()和 setMaximumPoolSize(),

  • setCorePoolSize:設定核心池大小
  • setMaximumPoolSize:設定執行緒池最大能建立的執行緒數目大小

當上述引數從小變大時,ThreadPoolExecutor 進行執行緒賦值,還可能立即建立新的執行緒來執行任務。

免費Java資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程,一共30G。
傳送門:mp.weixin.qq.com/s/JzddfH-7y…


相關文章