1. 程式人生 > 其它 >每日三道面試題,通往自由的道路12——執行緒池

每日三道面試題,通往自由的道路12——執行緒池

茫茫人海千千萬萬,感謝這一秒你看到這裡。希望我的面試題系列能對你的有所幫助!共勉!

願你在未來的日子,保持熱愛,奔赴山海!

每日三道面試題,成就更好自我

昨天既然聊到執行緒池中的實現方式,有些比較重要的我還沒問到。

1. 你知道ThreadPoolExecutor的構造方法和引數嗎

我們先來看看它的構造方法有哪些:

// 五個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {...}

// 六個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {...}

// 六個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {...}

// 七個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {...}

我們再來詳解下構造方法中涉及的7個引數,其中最重要5個引數就是第一個構造方法中的。

  • int corePoolSize:該執行緒池中核心執行緒數量

    核心執行緒:執行緒池中有兩類執行緒,核心執行緒和非核心執行緒。核心執行緒預設情況下會一直存在於執行緒池中,即使這個核心執行緒什麼都不幹,而非核心執行緒(臨時工)如果長時間的閒置,就會被銷燬。但是如果將

    allowCoreThreadTimeOut設定為true時,核心執行緒也是會被超時回收。

  • int maximumPoolSize:該執行緒池中允許存在的工作執行緒的最大數量。

    該值相當於核心執行緒數量 + 非核心執行緒數量。

  • long keepAliveTime

    :非核心執行緒閒置超時時長。

    非核心執行緒如果處於閒置狀態超過該值,就會被銷燬。如果設定allowCoreThreadTimeOut(true),則會也作用於核心執行緒。

  • TimeUnit unit:keepAliveTime的時間單位。

    TimeUnit是一個列舉型別 ,包括以下屬性:

    NANOSECONDS : 1微毫秒 
    MICROSECONDS : 1微秒
    MILLISECONDS : 1毫秒
    SECONDS : 秒 
    MINUTES : 分
    HOURS : 小時
    DAYS : 天
    
  • BlockingQueue workQueue:阻塞佇列,維護著等待執行的Runnable任務物件。

    當新任務來的時候,會先判斷當前執行執行緒數量是否達到了核心執行緒數,如果達到了,就會被存放在阻塞佇列中排隊等待執行。

    常用的幾個阻塞佇列:

    1. ArrayBlockingQueue

      陣列阻塞佇列,底層資料結構是陣列,需要指定佇列的大小。

    2. SynchronousQueue

      同步佇列,內部容量為0,每個put操作必須等待一個take操作,反之亦然。

    3. DelayQueue

      延遲佇列,該佇列中的元素只有當其指定的延遲時間到了,才能夠從佇列中獲取到該元素 。

    4. LinkedBlockingQueue

      鏈式阻塞佇列,底層資料結構是連結串列,預設大小是Integer.MAX_VALUE,也可以指定大小。

還有兩個非必須的引數:

  • ThreadFactory threadFactory

    建立執行緒的工廠 ,用於批量建立執行緒,統一在建立執行緒時設定一些引數,如是否守護執行緒、執行緒的優先順序等。如果不指定,會新建一個預設的執行緒工廠。

  • RejectedExecutionHandler handler

    拒絕處理策略,線上程數量大於最大執行緒數後就會採用拒絕處理策略,四種拒絕處理的策略為 :

    1. ThreadPoolExecutor.AbortPolicy預設拒絕處理策略,丟棄任務並丟擲RejectedExecutionException異常。
    2. ThreadPoolExecutor.DiscardPolicy:丟棄新來的任務,但是不丟擲異常。
    3. ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列頭部(最舊的)的任務,然後重新嘗試執行程式(如果再次失敗,重複此過程)。
    4. ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務。

不錯呀!執行緒池的引數也有深入瞭解,那咱們繼續

2. 你可以說下執行緒池的執行過程原理嗎

昨天MyGirl跟我講了一下她去銀行辦理業務的一個場景:

  1. 首先MyGirl(任務A)先去銀行(執行緒池)辦理業務,她發現她來早了,現在銀行才剛開門,櫃檯視窗服務員還沒過來(相當於執行緒池中的初始執行緒為0),此時銀行經理看到MyGirl來了,就安排她去一號櫃檯視窗並安排了1號正式工作人員來接待她。
  2. 在MyGirl的業務還沒辦完時,一個不知名的路人甲(任務B)出現了,他也是要來銀行辦業務,於是銀行經理安排他去二號櫃檯並安排了2號正式工作人員。假設該銀行的櫃檯視窗就只有兩個(核心執行緒數量2)。
  3. 緊接著,在所有人業務都還沒做完的情況,持續來個三個不知名的路人乙丙丁,他們也是要來辦業務的,但是由於櫃檯滿了,安排了他們去旁邊的銀行大廳的座位上(阻塞佇列,這裡假設大小為3)等候並給了對應順序的號碼,說等前面兩個人辦理完後,按順序叫號你們呦,請注意聽。
  4. 過一會,一個路人戊也想來銀行辦理業務,而經理看到櫃檯滿了,座位滿了,只能安排了一個臨時工(非核心執行緒,這裡假設最大執行緒為3,即非核心為1)手持pad裝置並給路人戊去辦理業務。
  5. 而此時,一個路人戌過來辦理業務,而經理看到櫃檯滿了,座位滿了,臨時工也安排滿了(最大執行緒數+阻塞佇列都滿了),無奈經理只能掏出一本《如何接待超出最大限度的手冊》,選擇拒接接待路人戌通知他,過會再來吧您嘞,這裡已經超負荷啦!
  6. 最後,相繼所有人的業務都辦完了,現在也沒人再來辦業務,並且臨時工的空閒時間也超過了1小時以上了(最大空閒時間預設60秒),經理讓臨時工都先下班回家了(銷燬執行緒)。
  7. 但是一個銀行要保證正常的執行,只能讓正式員工繼續上班,不得提早下班。

而實際上執行緒的流程原理跟這個一樣,我們來看下處理任務的核心方法execute,它的原始碼大概是什麼樣子的呢,當然我們也可以看原始碼中的註釋,裡面也寫的很清楚。這裡具體講下思路。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();  
    // 1. 獲取ctl,ctl是記錄著執行緒池狀態和執行緒數。
    int c = ctl.get();
    // 2. 判斷當前執行緒數小於corePoolSize核心執行緒,則呼叫addWorker建立核心執行緒執行任務
    if (workerCountOf(c) < corePoolSize) {
       if (addWorker(command, true))
           return;
       // 建立執行緒失敗,需要重新獲取clt的狀態和執行緒數。
       c = ctl.get();
    }
    // 3. 如果不小於corePoolSize,進入下面的方法。
    // 判斷執行緒池是否執行狀態並且執行執行緒數大於corePoolSize,將任務新增到workQueue佇列。
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 3.1 再次檢查執行緒池是否執行狀態。
        // 如果isRunning返回false(狀態檢查),則remove這個任務,然後執行拒絕策略。
        if (! isRunning(recheck) && remove(command))
            reject(command);
            // 3.2 執行緒池處於running狀態,但是沒有執行緒,則建立執行緒加入到執行緒池中
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 4. 如果放入workQueue失敗,則建立非核心執行緒執行任務,
    // 如果這時建立非核心執行緒失敗(當前執行緒總數不小於maximumPoolSize時),就會執行拒絕策略。
    else if (!addWorker(command, false))
         reject(command);
}

我們可以大概看下思路圖:

先解釋下ctl

變數ctl定義為AtomicInteger,記錄了“執行緒池中的任務數量”和“執行緒池的狀態”兩個資訊。以高三位記錄著執行緒池的狀態和低29位記錄執行緒池中的任務數量。

RUNNING : 111
SHUTDOWN : 000
STOP : 001
TIDYING : 010
TERMINATED : 011

最後總結一下執行過程:

  1. 任務到達時,會先判斷核心執行緒是否滿了,不滿則呼叫addWorker方法建立核心執行緒執行任務。
  2. 然後會判斷下執行緒池中的執行緒數 < 核心執行緒,無論執行緒是否空閒,都會新建一個核心執行緒執行任務(讓核心執行緒數量快速達到核心執行緒總數)。此步驟會開啟鎖mainLock.lock();
  3. 而線上程池中的執行緒數 >= 核心執行緒時,新來的執行緒任務會進入任務阻塞佇列中等待,然後空閒的核心執行緒會依次去阻塞佇列中取任務來執行。
  4. 當阻塞佇列滿了,說明這個時候任務很多了,此時就需要一些非核心執行緒臨時工來執行這些任務了。於是會建立非核心執行緒去執行這個任務。
  5. 最後當阻塞佇列滿了, 且匯流排程數達到了maximumPoolSize,則會採取拒絕策略進行處理。
  6. 當非核心執行緒取任務的時間達到keepAliveTime還沒有取到任務即空閒時間,就會回收非核心執行緒。

不錯,這個執行過程原理都有深入瞭解過,最後問你一道:

3. 能否寫一個簡單執行緒池的demo?

你這怕不是魔鬼吧,寫一個執行緒池。不過簡單的執行緒池還是可以寫寫滴!當然通過上面引數,執行過程的學習,寫出來一個還是比較So Easy的。只是如果真的到面試了,真的讓你手敲,可能就忘了,還是得多敲。

這裡還是直接用簡單的ThreadPoolExecutor建立吧,等後續寫執行緒池相關文章,再詳細寫自己建立的執行緒池吧。

我們先建立一個任務類Task:

/**
 * 自定義任務類
 */
public class Task implements Runnable{

    private int id;

    public Task(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "即將執行的任務是" + id + "任務");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "執行完成的任務是" + id + "任務");
    }
}

測試程式碼:

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 3;
    private static final int MAX_POOL_SIZE = 5;
    private static final int QUEUE_CAPACITY = 10;
    private static final Long KEEP_ALIVE_TIME = 1l;


    public static void main(String[] args) {
        //通過ThreadPoolExecutor建構函式自定義引數建立
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            Task task = new Task( i);
            //執行Runnable
            executor.execute(task);
        }
        //終止執行緒池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("執行緒已經全部執行完");
    }
}

得到的結果:

pool-1-thread-1即將執行的任務是0任務
pool-1-thread-3即將執行的任務是2任務
pool-1-thread-2即將執行的任務是1任務
pool-1-thread-1執行完成的任務是0任務
pool-1-thread-3執行完成的任務是2任務
pool-1-thread-1即將執行的任務是3任務
pool-1-thread-3即將執行的任務是4任務
pool-1-thread-2執行完成的任務是1任務
pool-1-thread-2即將執行的任務是5任務
pool-1-thread-3執行完成的任務是4任務
pool-1-thread-1執行完成的任務是3任務
pool-1-thread-3即將執行的任務是6任務
pool-1-thread-1即將執行的任務是7任務
pool-1-thread-2執行完成的任務是5任務
pool-1-thread-2即將執行的任務是8任務
pool-1-thread-3執行完成的任務是6任務
pool-1-thread-1執行完成的任務是7任務
pool-1-thread-3即將執行的任務是9任務
pool-1-thread-2執行完成的任務是8任務
pool-1-thread-3執行完成的任務是9任務
執行緒已經全部執行完

當然此版寫的稍微簡單,但是如果真的忘記,也可以這麼寫。如果後續想看更多東西,可以關注我呀,我會持續更新內容!

小夥子不錯嘛!今天就到這裡,期待你明天的到來,希望能讓我繼續保持驚喜!

參考資料:執行緒池原理

注: 如果文章有任何錯誤和建議,請各位大佬盡情留言!如果這篇文章對你也有所幫助,希望可愛親切的您給個三連關注下,非常感謝啦!