Java的執行緒池是怎麼回事?來看看這篇文章吧

語言: CN / TW / HK

theme: healer-readable

作者:孫玉昌,暱稱【一一哥】,另外【壹壹哥】也是我哦

CSDN部落格專家、萬粉博主、阿里雲專家博主、掘金優質作者


一. 執行緒池的思想

我們使用執行緒的時候就去建立一個執行緒,這樣實現起來非常簡便,但是就會有一個問題:

如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒需要時間。

那麼有沒有一種辦法使得執行緒可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?在Java中可以通過執行緒池來達到這樣的效果。

二. 執行緒池的概念

執行緒池: 其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。

三. 執行緒池的好處

  1. 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後宕機)

四. 執行緒池 核心 API

1. 執行緒池API類關係

Java裡面執行緒池的頂級介面是java.util.concurrent.Executor,但是嚴格意義上講Executor並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是java.util.concurrent.ExecutorService,最終實現類為ThreadPoolExecutor,如下圖所示:

2. ThreadPoolExecutor的構造方法

ThreadPoolExecutor提供了四個建構函式(後附引數詳解):

``` //五個引數的建構函式 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

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

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

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

要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,很有可能配置的執行緒池不是較優的,因此在java.util.concurrent.Executors執行緒工廠類裡面提供了一些靜態工廠,生成一些常用的執行緒池。官方建議使用Executors工廠類來建立執行緒池物件。

3. 建立執行緒池的4種方法

Executors類中有個建立執行緒池的方法如下(四種執行緒池):

  • public static ExecutorService newFixedThreadPool(int nThreads):返回執行緒池物件。(建立的是有界執行緒池,也就是池中的執行緒個數可以指定最大數量)

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

  • 建立快取執行緒池(由任務的多少來決定)newCachedThreadPool()

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

  • 建立單執行緒池 newSingleThreadExecutor(),有且僅有一個工作執行緒執行任務

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

  • 建立排程執行緒池(排程:按週期、定時執行)newScheduledThreadPool(int corePoolSize)

``` public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }

public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); } ```

五. 執行緒池引數詳解

1. 核心引數

Java執行緒池有以下幾個核心引數:

  • int corePoolSize: 該執行緒池中核心執行緒數最大值 核心執行緒: 執行緒池新建執行緒的時候,如果當前執行緒總數小於corePoolSize,則新建的是核心執行緒,如果超過corePoolSize,則新建的是非核心執行緒核心執行緒預設情況下會一直存活線上程池中,即使這個核心執行緒啥也不幹(閒置狀態)。 如果指定ThreadPoolExecutor的allowCoreThreadTimeOut這個屬性為true,那麼核心執行緒如果不幹活(閒置狀態)的話,超過一定時間(時長下面引數決定),就會被銷燬掉。
  • int maximumPoolSize: 該執行緒池中執行緒總數最大值 執行緒總數 = 核心執行緒數 + 非核心執行緒數。
  • long keepAliveTime: 該執行緒池中非核心執行緒閒置超時時長 一個非核心執行緒,如果不幹活(閒置狀態)的時長超過這個引數所設定的時長,就會被銷燬掉,如果設定allowCoreThreadTimeOut = true,則會作用於核心執行緒。
  • TimeUnit unit: keepAliveTime的單位。TimeUnit是一個列舉型別,包括:NANOSECONDS:1微毫秒 = 1微秒 / 1000 MICROSECONDS:1微秒 = 1毫秒 / 1000 MILLISECONDS:1毫秒 = 1秒 /1000 SECONDS:秒 MINUTES:分 HOURS:小時 DAYS:天
  • BlockingQueue workQueue: 該執行緒池中的任務佇列:維護著等待執行的Runnable物件。當所有的核心執行緒都在幹活時,新新增的任務會被新增到這個佇列中等待處理,如果佇列滿了,則新建非核心執行緒執行任務。

2. 常用的workQueue型別

SynchronousQueue:這個佇列接收到任務的時候,會直接提交給執行緒處理,而不保留它,如果所有執行緒都在工作怎麼辦?那就新建一個執行緒來處理這個任務!所以為了保證不出現<執行緒數達到了maximumPoolSize而不能新建執行緒>的錯誤,使用這個型別佇列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大。

LinkedBlockingQueue:這個佇列接收到任務的時候,如果當前執行緒數小於核心執行緒數,則新建執行緒(核心執行緒)處理任務;如果當前執行緒數等於核心執行緒數,則進入佇列等待。由於這個佇列沒有最大值限制,即所有超過核心執行緒數的任務都將被新增到佇列中,這也就導致了maximumPoolSize的設定失效,因為匯流排程數永遠不會超過corePoolSize。

ArrayBlockingQueue:可以限定佇列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了maximumPoolSize,並且佇列也滿了,則發生錯誤。

DelayQueue:佇列內元素必須實現Delayed介面,這就意味著你傳進去的任務必須先實現Delayed介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務。

ThreadFactory threadFactory: 建立執行緒的方式,這是一個介面,你new他的時候需要實現他的Thread newThread(Runnable r)方法,一般用不上。

RejectedExecutionHandler handler: 丟擲異常專用的,比如上面提到的兩個錯誤發生了,就會由這個handler丟擲異常。

六. 執行緒池的 執行 策略

當一個任務被新增進執行緒池時,會執行以下策略:

  • 執行緒數量未達到corePoolSize,則新建一個執行緒(核心執行緒)執行任務;
  • 執行緒數量達到了corePools,則將任務移入佇列等待;
  • 佇列已滿,新建執行緒(非核心執行緒)執行任務;
  • 佇列已滿,匯流排程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)丟擲異常。

如果我們獲取到了一個執行緒池ExecutorService物件,又該怎麼使用呢?我們可以在這裡定義一個使用執行緒池物件的方法,如下所示:

public Future<?> submit(Runnable task):獲取執行緒池中的某一個執行緒物件,並執行任務。

public <T> Future<T> submit(Callable<T> task):獲取執行緒池中的某一個執行緒物件,並執行任務。

這裡的Future介面,帶有返回值,可以用來記錄執行緒任務執行完畢後產生的結果。

. **執行緒池 使用

1. 使用步驟

如果我們要使用執行緒池中的執行緒物件,主要是遵循如下幾個步驟:

  1. 建立執行緒池物件;
  2. 建立Runnable/Callable介面子類物件;
  3. 提交Runnable/Callable介面子類物件;
  4. 關閉執行緒池(一般不做)。

2. 程式碼案例

以下是執行緒池的使用案例:

``` public class ExecutorServiceDemo { public static void main(String[] args) throws Exception { //獲取到了執行緒池 固定長度的執行緒池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100); //可快取的執行緒池 //ExecutorService executorService = Executors.newCachedThreadPool(); //單例執行緒池 //ExecutorService fixedThreadPool = Executors.newSingleThreadExecutor(); //任務排程執行緒池 // ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

    //建立任務
    MyCallable myCallable =new MyCallable();
    Future<String> submit = fixedThreadPool.submit(myCallable); //future代表的是執行緒執行之後的返回結果
    fixedThreadPool.submit(myCallable);//執行任務
    fixedThreadPool.submit(myCallable);
    fixedThreadPool.submit(myCallable);
    fixedThreadPool.submit(myCallable);
    //關閉執行緒池  服務端執行緒池是不能關閉的
    //fixedThreadPool.shutdown();
}

}

//callable可以得到執行緒的返回值 public class MyCallable implements Callable { @Override public String call() throws Exception { System.out.println(Thread.currentThread().getName()+"call任務執行了"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+"call執行完畢"); return "qfedu"; } } ```

現在你知道Java中的執行緒池是怎麼回事了嗎?如果你還有其他問題,可以在評論區留言哦。