1. 程式人生 > 程式設計 >JAVA執行緒池專題(概念和作用)

JAVA執行緒池專題(概念和作用)

執行緒池的作用

我們在用一個東西的時候,首先得搞明白一個問題。這玩意是幹嘛的,為啥要用這個,用別的不行嗎。那麼一個一個解決這些問題

我們之前都用過資料庫連線池,執行緒池的作用和連線池有點類似,頻繁的建立,銷燬執行緒會造成大量的不必要的效能開銷,所以這個時候就出現了一個東西統一的管理執行緒,去負責執行緒啥時候銷燬,啥時候建立,以及維持執行緒的狀態,當程式需要使用執行緒的時候,直接從執行緒池拿,當程式用完了之後,直接把執行緒放回執行緒池,不需要去管執行緒的生命週期,專心的執行業務程式碼就行。

當然,如果非要是自己想手動new一個執行緒來執行,也不是不可以,只是像上面說的那樣,第一麻煩,第二開銷大,第三不好控制。

控制執行緒的方法

在說到執行緒池之前,首先要提到一個建立執行緒池的工具類,又或者說是工廠類 Executors 通過這個執行緒可以統一的建立執行緒,返回的是一個ExecutorService 類這個類中包含了一些對執行緒執行過程進行管理控制的方法;

void execute(Runnable command); 這個方法是將任務提交到執行緒池進行執行。這個方法沒有返回值。

<T> Future<T> submit(Callable<T> task); 這個方法最特別的地方是執行緒執行完畢之後是有返回值的,另外方法的引數可以用Callable也可以為Runnable。可以適用於一些後續的程式碼,需要執行緒執行結果的程式。

下面的示例中,我們建立了一個 ExecutorService 的例項,提交了一個任務,然後使用返回的 Future 的 get() 方法等待提交的任務完成並返回值。

  ExecutorService executorService = Executors.newFixedThreadPool(10);
  Future<String> future = executorService.submit(() -> "Hello World");
  // 一些其它操作
  String result = future.get();

在實際使用時,我們並不會立即呼叫 future.get()

方法,可能會等待一些時間,推遲呼叫它直到我們需要它的值用於計算等目的。

ExecutorService 中的 submit() 方法被過載為支援 RunnableCallable ,它們都是功能介面,可以接收一個 lambdas 作為引數( 從 Java 8 開始 ):

  • 使用 Runnable 作為引數的方法不會丟擲異常也不會返回任何值 ( 返回 void )
  • 使用 Callable 作為引數的方法則可以丟擲異常也可以返回值。

如果想讓編譯器將引數推斷為 Callable 型別,只需要 lambda 返回一個值即可。

  • void shutdown(); 在呼叫了shutdown方法之後,執行緒池就不會再接收新的任務,此時執行緒池還沒有停止,仍然會把執行緒池中國正在執行但是還沒有執行完的任務繼續執行完畢,那些沒有開始執行的任務則被中斷
  • List<Runnable> shutdownNow(); 在呼叫了shutdownNow方法之後,會將執行緒池的狀態設定為stop,正在執行的任務則被停止,沒被執行任務的則返回。

這兩種方法的使用場景:如果執行緒中的任務相互之間沒有什麼關聯某個執行緒的異常對結果影響不大。那麼所有執行緒都能在執行任務結束之後可以正常結束,程式能在所有task都做完之後正常退出,適合用ShutDown。但是,如果一個執行緒在做某個任務的時候失敗,則整個結果就是失敗的,其他worker再繼續做剩下的任務也是徒勞,這就需要讓他們全部停止當前的工作。這裡使用ShutDownNow就可以讓該pool中的所有執行緒都停止當前的工作,從而迫使所有執行緒執行退出。從而讓主程式正常退出。

執行緒池的分類

通過工廠類 Executors 通過這個執行緒可以根據自己的需要統一的建立各種型別的執行緒,執行緒的分類大致分為以下四種:

  1. newSingleThreadExecutor
  2. CachedThreadPool
  3. newFixedThreadPool
  4. newScheduledThreadPool
  • newSingleThreadExecutor 建立一個單執行緒的執行緒池,核心執行緒和最大執行緒都為1,因此只會有一個工作執行緒,會按照指定順序去執行,而且空閒時間為0,說明一旦沒有任務了,執行緒就會被銷燬
  • public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
      }
    
    
    public class SinglePoolDemo {
      public static void main(String[] args) {
        ExecutorService pool = Executors.newSingleThreadExecutor();
    //    ExecutorService pool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) {
          int finalI = i;
          pool.execute(() -> {
            System.out.println(Thread.currentThread().getName()+"----"+ finalI);
          });
        }
      }
    }
    

輸出結果:

pool-1-thread-1----0
pool-1-thread-1----1
pool-1-thread-1----2
pool-1-thread-1----3
pool-1-thread-1----4
pool-1-thread-1----5
pool-1-thread-1----6
pool-1-thread-1----7
pool-1-thread-1----8
pool-1-thread-1----9

觀察執行緒編號,可以發現,自始自終都只有一個執行緒在執行,並且也是按照順序來執行的,。

JAVA執行緒池專題(概念和作用)

  • CachedThreadPool 建立一個按需建立的執行緒,核心執行緒數為0,有一個最大執行緒數量,意味著可以根據實際任務數的需要,靈活的建立和管理執行緒,keepAlive時間為60s,代表當某執行緒超過60s空閒的時候,才會被銷燬,這個執行緒池最特殊的地方在於,同步佇列最多隻能有一個元素,加入佇列的執行緒會被馬上執行。
  •  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
      }
    
    public class CachePoolDemo {
      public static void main(String[] args) {
    
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 20000; i++) {
          int finalI = i;
          pool.submit(() -> {
            System.out.println(Thread.currentThread().getName()+"-------------"+finalI);
          });
        }
      }
    }
    

執行結果部分:

......
pool-1-thread-1805-------------19760
pool-1-thread-1806-------------19783
pool-1-thread-1809-------------19875
pool-1-thread-1810-------------19951
pool-1-thread-1811-------------19980

以上的程式碼我們運行了2w次執行緒任務,如果是按照我們之前的做法的話,我們要new 2w的執行緒去執行。通過這個不定長的執行緒池,他可以根據任務數來靈活的分配所建立的執行緒,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒,所以這裡只建立了大概1800多個執行緒就完成了我們原本需要new 2w個執行緒才能完成的任務,之所以說他是靈活分配的是因為,可以這樣驗證看看,把i的值改為20的話,所建立的執行緒數量大概是10以內,因此是根據任務數量來自行建立執行緒數的,可以保證效率和效能的最大化。

但是經過實測,這個靈活性雖然最高,但是效能貌似是相對比較差的,在兩萬任務數的條件下,所以他的缺點就是,可能會建立大量的執行緒。當然執行緒池這東西是需要根據自身情況來選擇的。如果主執行緒提交任務的速度遠遠大於CachedThreadPool的處理速度,則CachedThreadPool會不斷地建立新執行緒來執行任務,這樣有可能會導致系統耗盡CPU和記憶體資源,所以在使用該執行緒池是,一定要注意控制併發的任務數,否則建立大量的執行緒可能導致嚴重的效能問題。

JAVA執行緒池專題(概念和作用)

  • newFixedThreadPool 可以通過傳入一個int引數來指定建立一個定長的執行緒池,該執行緒池的核心執行緒數和最大執行緒數都是你傳進去的引數的值,存活時間都為0說明只要任務空閒下來了,就會被銷燬,阻塞佇列的最大值為MAX_VALUE。所以他的缺點是,可能會將大量的時間花在處理堆積的請求阻塞佇列中的執行緒。
  public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads,nThreads,new LinkedBlockingQueue<Runnable>());
  }
public class FixedPoolDemo {
  public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(10);
//    ExecutorService pool = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 1000; i++) {
      int finalI = i;
      pool.execute(() -> {
        System.out.println(Thread.currentThread().getName()+"----"+ finalI);
      });
    }
  }
}

從執行結果可以看出,執行緒池一直都是維持著十個執行緒

.....
pool-1-thread-5----882
pool-1-thread-1----881
pool-1-thread-4----865
pool-1-thread-10----989
pool-1-thread-3----931
pool-1-thread-2----934
pool-1-thread-9----910
pool-1-thread-6----896

JAVA執行緒池專題(概念和作用)

  • newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
  public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize,NANOSECONDS,new DelayedWorkQueue());
  }

以上四種執行緒池,各有優劣點

newFixedThreadPool、newSingleThreadExecutor:

主要問題是堆積的請求處理佇列可能會耗費非常大的記憶體,甚至OOM。

newCachedThreadPool、newScheduledThreadPool:

主要問題是執行緒數最大數是Integer.MAX_VALUE,可能會建立數量非常多的執行緒,甚至OOM。

阿里執行緒池規範

  1. 執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
  2. FixedThreadPool 和 SingleThreadPool: 允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
  3. CachedThreadPool 和 ScheduledThreadPool: 允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

總結

本篇文章首先我們知道了執行緒池有什麼好處,然後瞭解一些執行緒的執行方法,submit,execute,shutdown以及他們的區別,用法等等,然後對幾種執行緒池做了一個大概的介紹,以及他們的作用,好處和弊端。如果看的細心的同學可以看程式碼發現,這些執行緒池其實本質上都是通過建立一個 ThreadPoolExecutor ,包括阿里的執行緒池規範也是建議用ThreadPoolExecutor ,但是本篇文章只是對執行緒池的作用以及分類做一個概述,在下篇文章中,將會詳細的講一下ThreadPoolExecutor

以上就是JAVA執行緒池專題(概念和作用)的詳細內容,更多關於Java執行緒池的概念和作用的資料請關注我們其它相關文章!