1. 程式人生 > >多執行緒--執行緒池的正確開啟方式

多執行緒--執行緒池的正確開啟方式

概述

執行緒可認為是作業系統可排程的最小的程式執行序列,一般作為程序的組成部分,同一程序中多個執行緒可共享該程序的資源(如記憶體等)。JVM執行緒跟核心輕量級程序有一對一的對映關係,所以JVM中的執行緒是很寶貴的。

一般在工程上多執行緒的實現是基於執行緒池的。因為相比自己建立執行緒,多執行緒具有以下優點

  • 執行緒是稀缺資源,使用執行緒池可以減少建立和銷燬執行緒的次數,每個工作執行緒都可以重複使用。
  • 可以根據系統的承受能力,調整執行緒池中工作執行緒的數量,防止因為消耗過多記憶體導致伺服器崩潰。

Executors存在什麼問題

看阿里巴巴開發手冊併發程式設計這塊有一條:執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式。

Executors為什麼存在缺陷

1. 執行緒池工作原理

當一個任務通過execute(Runnable)方法欲新增到執行緒池時:

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

2.newFixedThreadPool分析

Java中的BlockingQueue主要有兩種實現,分別是ArrayBlockingQueueLinkedBlockingQueue

ArrayBlockingQueue是一個用陣列實現的有界阻塞佇列,必須設定容量。

LinkedBlockingQueue

是一個用連結串列實現的有界阻塞佇列,容量可以選擇進行設定,不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer.MAX_VALUE

這裡的問題就出在:不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設定LinkedBlockingQueue的容量的話,其預設容量將會是Integer.MAX_VALUE

newFixedThreadPool中建立LinkedBlockingQueue時,並未指定容量。此時,LinkedBlockingQueue就是一個無邊界佇列,對於一個無邊界佇列來說,是可以不斷的向佇列中加入任務的,這種情況下就有可能因為任務過多而導致記憶體溢位問題。

3. newCachedThreadPool分析

結合上述流程圖,核心執行緒數=0,最大執行緒無限大,由於SynchronousQueue是一個快取值為1的阻塞佇列。當有大量任務請求時,執行緒池會建立大量執行緒,造成OOM。

執行緒池引數詳解

1. 構造方法

/**
 * @param corePoolSize  核心執行緒數
 * @param maximumPoolSize 最大執行緒數
 * @param keepAliveTime 執行緒所允許的空閒時間
 * @param unit 執行緒所允許的空閒時間的單位
 * @param workQueue  執行緒池所使用的緩衝佇列
 * @param handler 執行緒池對拒絕任務的處理策略
 */
ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)

2. 執行緒池拒絕策略

RejectedExecutionHandler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。。以下是JDK1.5提供的四種策略。

  • AbortPolicy:直接丟擲異常
  • CallerRunsPolicy:只用呼叫者所線上程來執行任務。
  • DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
  • DiscardPolicy:不處理,丟棄掉。
  • 當然也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化不能處理的任務。

執行緒池正確開啟方式

1. 建立執行緒池

避免使用Executors建立執行緒池,主要是避免使用其中的預設實現,那麼我們可以自己直接呼叫ThreadPoolExecutor的建構函式來自己建立執行緒池。在建立的同時,給BlockQueue指定容量就可以了。

        ThreadPoolExecutor executorService = new ThreadPoolExecutor(8,
                16,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10));

2. 向執行緒池提交任務

我們可以使用execute提交的任務,但是execute方法沒有返回值,所以無法判斷任務知否被執行緒池執行成功。通過以下程式碼可知execute方法輸入的任務是一個Runnable類的例項。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("執行緒池無返回結果");
            }
        });複製ErrorCopied

我們也可以使用submit 方法來提交任務,它會返回一個future,那麼我們可以通過這個future來判斷任務是否執行成功,通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));

        Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "ok";
            }
        });
        System.out.println("執行緒池返回結果:" + future.get());

3. 關閉執行緒池

shutdown關閉執行緒池

方法定義:public void shutdown()

(1)執行緒池的狀態變成SHUTDOWN狀態,此時不能再往執行緒池中新增新的任務,否則會丟擲RejectedExecutionException異常。

(2)執行緒池不會立刻退出,直到新增到執行緒池中的任務都已經處理完成,才會退出。

注意這個函式不會等待提交的任務執行完成,要想等待全部任務完成,可以呼叫:

public boolean awaitTermination(longtimeout, TimeUnit unit)

shutdownNow關閉執行緒池並中斷任務

方法定義:public List shutdownNow()

(1)執行緒池的狀態立刻變成STOP狀態,此時不能再往執行緒池中新增新的任務。

(2)終止等待執行的執行緒,並返回它們的列表;

(3)試圖停止所有正在執行的執行緒,試圖終止的方法是呼叫Thread.interrupt(),但是大家知道,如果執行緒中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的執行緒的。所以,ShutdownNow()並不代表執行緒池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。

4. 如何配置執行緒池大小

CPU密集型任務

該任務需要大量的運算,並且沒有阻塞,CPU一直全速執行,CPU密集任務只有在真正的多核CPU上才可能通過多執行緒加速 CPU密集型任務配置儘可能少的執行緒數量:

CPU核數+1個執行緒的執行緒池

例如: CPU 16核,記憶體32G。執行緒數=16

IO密集型任務

IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如CPU核數*2

某大廠設定策略:IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數:

CPU核數/(1-阻塞係數)

例如: CPU 16核, 阻塞係數 0.9 ------------->16/(1-0.9) = 160 個執行緒數。

此時非阻塞執行緒=16

寫在最後

這篇文章是對執行緒池使用的簡單分析,為了更好的學習程式設計,後續會從原始碼的角度分析執行緒池的執行原理,上述文章如有錯漏,還望大家不吝賜教。

公眾號 【當我遇上你】