1. 程式人生 > 實用技巧 >題解 P1891 【瘋狂 LCM】

題解 P1891 【瘋狂 LCM】

1.原理

1.1為什麼使用執行緒池?

執行緒池體現了一種池化的思想,有些類似於mysql的連線池。我們在使用多執行緒的時候,阿里規約會建議我們使用執行緒池,而不是直接new Thread。我認為執行緒池主要體現在以下幾個優點:

  • 便於管理執行緒,比如,可以自定義控制執行緒的建立數量。因為可以實現執行緒的統一分配,呼叫及監控。
  • 降低系統的資源消耗,通過重用已存在的執行緒,降低執行緒建立和銷燬造成的消耗。
  • 提高響應的速度,這裡主要體現在任務到達時,不用再等待執行緒建立便可以直接呼叫。

1.2 執行緒池原理

執行緒池的主要執行流程如下:

ThreadPoolExecutor執行示意圖如下:

  1. corePool核心執行緒池是否已滿,如果未滿,則建立新執行緒來執行任務。

  2. corePool已滿,則將任務存至BlockingQueue工作佇列中。

  3. 若建立後的執行緒數量將大於maximumPoolSize,則呼叫RejectedExecutionHandler的策略來處理此類未執行的任務,若建立執行緒後的資料仍然小於等於maximumPoolSize,則直接建立執行緒來執行任務。

以上三步中,只有第二步不會要求獲取全域性鎖。在設計上已經盡力避免進行獲取全域性鎖的這個消耗操作,所以一般在第二步的時候就已經可以處理完成任務了。

為什麼設計的時候會區分核心執行緒池和最大執行緒池呢?

個人理解是為了追求效能與資源消耗的極致吧,假如核心執行緒池足以滿足任務執行的需求,那就不必再建立核心執行緒池之外的執行緒,同時,真的出現執行緒不夠用的時候,同樣也會給它一個機會來建立一個執行緒
執行。儘量不出現無法執行任務的情況。只是個人猜想,望指正。

2.使用

2.1 建立

構造方法

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:核心執行緒池大小。當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使存在空閒的核心執行緒能夠執行任務,其仍會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。

  • maximumPoolSize:最大執行緒數大小。執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是,如果使用了無界的任務佇列這個引數就沒什麼效果。

  • keepAliveTime:執行緒存活時間。這裡是指工作執行緒在執行任務進入空閒狀態的時候。當有需要任務需要頻繁執行的時候,可以調節此引數來使執行緒存活時間增加,避免頻繁建立新執行緒造成資源的浪費。

  • TimeUnit:執行緒存活時間單位。

  • ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個創建出來的執行緒設定更有意義的名字 。

  • workQueue:任務工作佇列。用於儲存等待執行的任務的阻塞佇列。有以下幾種:

    • ArrayBlockingQueue:基於陣列結構的有界阻塞佇列,FIFO的原則進行元素的排序。

    • LinkedBlockingQueue: 基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。

    • SynchronousQueue:不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列,個人理解此佇列為了保證每個任務儘可能的被執行處理。

    • PrirorityBlockingQueue:具有優先順序的無限阻塞佇列。

  • RejectedExecutionHandler:飽和策略。當工作佇列和執行緒池無法接收或執行任務時,則採用一種策略來處理提交的任務。

    • AbortPolicy:直接丟擲異常

    • CallerRunsPolicy:只用呼叫者所線上程來執行任務。

    • DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。

    • DiscardPolicy:不處理任務,直接丟棄掉。

2.2 執行

向執行緒池提交任務主要有兩個方法:

  • execute()方法

    提交一個不需要返回值的任務,所以無法判斷任務是否被執行緒池執行。例如:

            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    //do something
                }
            });
    
  • submit()方法

    可以返回任務執行的結果,通過Future物件來包裝執行的結果,Future物件可以獲取任務是否執行成功,任務執行的結果等。

                Future future = threadPoolExecutor.submit(()->{
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName());
               });
    

2.3 關閉

使用shutdown或shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷

執行緒,因此無法響應中斷的任務可能永遠無法終止。

二者的區別是,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任

務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒,算是我們常說的優雅的

關閉方式。

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時調

用isTerminaed方法會返回true。至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown方

法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow方法。

2.4 配置

技術離不開業務需求,所以線上程池的配置上也離不開實際的需求。比如:當需要處理提交頻繁的任務時,我們可以嘗試將執行緒的存活時間調大。再或者,如果為了保持應用的穩定性,我們可以使用有界的阻塞佇列,避免佇列中執行緒任務不斷積壓,導致記憶體崩潰。

參考:《Java併發程式設計的藝術》