1. 程式人生 > >java 執行緒池那點事兒

java 執行緒池那點事兒

1.為什麼要用執行緒池

執行緒池提供了一種任務的提交與任務的執行解偶的策略,並且有以下幾種優勢

提高資源利用率
通過複用自己的執行緒來降低建立執行緒和銷燬執行緒的開銷。
如果不用執行緒池而採用為每個任務都建立一個新執行緒的話會很浪費系統資源。因為建立執行緒和銷燬執行緒都是耗系統資源的行為。除此之外還會由於執行緒過多而導致JVM出現OutOfMemory

提高響應速度
當新來一個任務時,如果有空閒執行緒存在可立即執行任務,中間節省了建立執行緒的過程

統一管理執行緒
如果不用執行緒池來管理,而是無限建立執行緒的話不僅消耗系統資源,而且還會導致系統不穩定。使用執行緒池可以進行統一分配,調優以及監控。

2.常見執行緒池以及引數

2.1 建立執行緒池

通過Executors的工廠方法來建立執行緒池,比如建立一個固定執行緒數的執行緒池

ExecutorService executorService = Executors.newFixedThreadPool(5);

通過ThreadPoolExecutor的建構函式來建立執行緒池

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,
                                                 int maxmumPoolSize,
                                                 long keepAliveTime,
                                                 TimeUnit unit,
                                                 BolockingQueue<Runnable> workQueue,
                                                 ThreadFactory factory,
                                                 RejectedExecusionHandler handler);

官方比較推薦使用後者,因為可以靈活選擇引數配置,以及自定義配置

無論是採用那種方式建立一個執行緒池,它內部都是通過 ThreadPoolExecutor的建構函式來實現的。所以要想全面掌握執行緒池的各種特性以及效能的話需要對 ThreadPoolExecutor類深入理解,包括各種引數的意義,引數之間是如何配合工作的等等。

2.2 執行緒池引數

1) corePoolSize 核心執行緒數。預設情況下,當提交一個任務時執行緒池會新建一個執行緒池(及時有空閒執行緒存在),直到執行緒數量等於基本大小(也可以預初始化執行緒)
2) workQueue 一個存放任務的阻塞佇列,當執行緒池裡的基本執行緒都在忙著的時候,提交一個新任務的話會暫時存放到阻塞佇列,等待執行,常用的阻塞佇列有一下幾種

  • ArrayBlockingQueue 基於陣列實現的FIFO阻塞佇列(有限長度)
  • LinkedBlockingQueue 基於連結串列實現的FIFO阻塞佇列(無限長度)
  • SynchronousQueue 本身不儲存任務,當有任務來的時候會一直阻塞,直到執行緒去執行
  • PriorityBlockingQueue 具有優先順序的優先佇列

3) maximumPoolSize 最大執行緒數。當所有的核心執行緒都在執行並且阻塞佇列也滿了的話會建立額外的幾個執行緒來執行,這時候執行緒池裡的所有執行緒數量就是最大執行緒數量。如果執行緒池採用的是像LinkedBlockingQueue這種無界佇列的話,該引數不會起到作用
4) KeepAliveTime 和 TimeUnit 如果某個執行緒的空閒時間大於KeepAliveTime的時候會被標記為可回收, 並且當前執行緒池裡的數量大於核心執行緒數量的時候會被終止。所以該引數跟maximumPoolSize一樣,使用無界佇列的時候不起作用,TimeUnit是時間單位

5) RejectedExecusionHandler 飽和策略。如果我們的任務佇列滿了,並且執行緒池裡的執行緒數量已經達到了最大執行緒數,而且這些執行緒都不再空閒狀態。這時候新提交任務的話,無法去執行,所以需要一種飽和策略來去處理這些任務。Java提供了一下幾種策略

* AbortPolicy (預設策略)   直接拋異常,呼叫者需要根據自己的需求去捕獲處理異常
* DiscardPolicy   直接丟棄,不處理直接丟棄該任務
* DiscardOldestPolicy 丟棄佇列裡最近的一個任務,並執行當前任務。如果是優先佇列的話會丟棄優先順序最高的任務,所以不推薦與優先佇列一起組合使用
* CallerRunsPolicy 由呼叫者當前執行緒來執行該任務。

##### 2.3 常見執行緒池

Java類庫提供了靈活的建立執行緒池方法,可以通過呼叫Executors中的靜態工廠方法來建立一個執行緒池。

1) newFixedThreadPool 將建立一個固定執行緒數量的執行緒池,每當提交一個任務時就建立一個執行緒。直到達到執行緒池的最大數量。corePoolSize和maximumPoolSize值相同,並且採用了LinkedBlockingQueue,所以最大執行緒池數,飽和策略,存活時間等等引數都將不被用到。

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

2) newSingleThreadExecutor 一個但執行緒的Executor,它跟上面的newFixedThreadPool類似,區別在於只有一個工作執行緒。通過該執行緒池可以確保有序的執行佇列中的任務,因為只有一個工作執行緒,所以出隊的順序就是執行的順序。

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

3) newCachedThreadPool 此執行緒池的執行緒數量沒有顯式的限制預設為Integer.MAX_VALUE,可以為每個任務建立一個執行緒來處理,但是它沒這麼做,它是具有快取執行緒的功能,是一種可伸縮的。比如,當處理新任務是首先看有沒有空閒可用的執行緒來處理,如果沒有的話才會新建立。並且當建立的執行緒空閒一段時間之後會回收(預設一分鐘)

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

4) newScheduledThreadPool
建立一個固定執行緒數的執行緒池,並且以延遲或定時的方式來執行。

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

3.執行流程

執行緒池執行任務是由Executor介面的execute()方法來執行的,下面看下執行任務流程的一段核心程式碼(java8)

    public void execute(Runnable command) {

        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);
    }

執行緒池的執行流程由以下幾個步驟來完成(每種執行緒池的實現略有不同,但核心思想相似)

1)首先,如果當前工作的執行緒數量小於核心執行緒數,則新建立一個執行緒來執行。如果核心執行緒池數滿了,

2)判斷執行緒池的任務佇列是否已滿,如果沒滿的話把該任務放到任務佇列裡,等待核心執行緒空閒之後去執行,如果滿了的話進入下一階段

3)判斷判斷執行緒池裡工作的執行緒數量是否達到了最大執行緒數,如果沒達到則建立一個新的執行緒來去執行。否則,採用飽和策略來處理

如果進去看內部實現的話會發現,執行緒池會把每個工作執行緒包裝成Worker,而把需要執行的任務包裝成Task來執行。

4.健康檢查

在專案中使用執行緒池的時候有必要對執行緒池進行監控,這樣可以根據執行緒池的使用狀況快速定位問題。可以通過執行緒池提供的引數進行監控,在監控執行緒池的時候可以使用以下屬性。

  • taskCount 執行緒池需要執行的任務數量
  • completedTaskCount 已經成功執行的任務數
  • largestPoolSize 曾經出現的最多執行緒數
  • getPoolSize 獲取執行緒數
  • getActiveCount 活動執行緒數

示例程式碼

public class Task  implements Runnable{
    private int i;

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

    @Override
    public void run() {
        System.out.println("task num ="+i);
    }
}
public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
        for (int i=0;i<5;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("完成任務數= "+pool.getCompletedTaskCount()+"  任務數= "+pool.getTaskCount()+" " +
                    ""+"  活動執行緒數="+pool.getActiveCount() +"" +"  執行緒數="+pool.getPoolSize() +"  "+"  最大執行緒數="+pool.getLargestPoolSize() );

            Task task = new Task(i);
            pool.execute(task);
        }
        pool.shutdown();

    }
}

列印結果如下:

完成任務數= 0  任務數= 0   活動執行緒數=0  執行緒數=0    最大執行緒數=0
task num =0
完成任務數= 1  任務數= 1   活動執行緒數=0  執行緒數=1    最大執行緒數=1
task num =1
完成任務數= 2  任務數= 2   活動執行緒數=0  執行緒數=2    最大執行緒數=2
task num =2
完成任務數= 3  任務數= 3   活動執行緒數=0  執行緒數=3    最大執行緒數=3
task num =3
完成任務數= 4  任務數= 4   活動執行緒數=0  執行緒數=4    最大執行緒數=4
task num =4