聊聊併發(五)——執行緒池

L發表於2021-11-18

一、概述

1、介紹

  在使用執行緒時,需要new一個,用完了又要銷燬,這樣頻繁的建立和銷燬很耗資源,所以就提供了執行緒池。道理和連線池差不多,連線池是為了避免頻繁的建立和釋放連線,所以在連
接池中就有一定數量的連線,要用時從連線池拿出,用完歸還給連線池,執行緒池也一樣。
  執行緒池:一種執行緒使用模式。執行緒過多會帶來排程開銷,進而影響快取區域性性和整體效能。而執行緒池維護著多個執行緒,等待著監督管理者分配可併發執行的任務。這避免了在處理短時間任務時建立與銷燬執行緒的代價。執行緒池不僅能夠保證核心的充分利用,還能防止過分排程。

  腦圖:https://www.processon.com/view/link/61849ba4f346fb2ecc4546e5

2、簡單使用

  執行緒池用法很簡單, 分為三步。首先用工具類Executors建立執行緒池,然後給執行緒池分配任務,最後關閉執行緒池就行了。

 1 public class ThreadPoolTest {
 2     public static void main(String[] args) throws Exception {
 3 
 4         // 1.建立一個 10 個執行緒數的執行緒池
 5         ExecutorService service = Executors.newFixedThreadPool(10);
 6 
 7         // 2.執行一個Runnable
 8         service.execute(new Number1());
 9 
10         // 2.提交一個Callable
11         Future<Integer> future = service.submit(new Number2());
12         Integer integer = future.get();
13         System.out.println("result = " + integer);
14 
15         // 關閉執行緒池
16         service.shutdown();
17     }
18 }
19 
20 class Number1 implements Runnable {
21 
22     @Override
23     public void run() {
24         System.out.println("----列印Runnable----");
25     }
26 }
27 
28 // 10以內數求和
29 class Number2 implements Callable<Integer> {
30     private int sum = 0;
31 
32     @Override
33     public Integer call() {
34         for (int i = 0; i <= 10; i++) {
35             if (i % 2 == 0) {
36                 sum += i;
37             }
38         }
39         return sum;
40     }
41 }

  注意:執行緒用完,要關閉執行緒池,否則程式依然在執行中

3、相關API

  JDK 5.0 起提供了執行緒池相關API:頂級介面Executor,及子介面 ExecutorService 和工具類Executors。

  JUC包描述:圖片來源API文件

  Executors:工具類,執行緒池的工廠類,用於建立並返回不同型別的執行緒池。

 1 // 一池N執行緒:建立一個固定(可重用)執行緒數的執行緒池。
 2 ExecutorService Executors.newFixedThreadPool(int nThreads)
 3 
 4 // 一池一執行緒:建立一個只有一個執行緒的執行緒池。
 5 ExecutorService Executors.newSingleThreadExecutor()
 6 
 7 // 可擴容:建立一個可根據需要執行緒數,建立新的執行緒的執行緒池。
 8 ExecutorService Executors.newCachedThreadPool()
 9 
10 // 可用於排程:建立一個執行緒池,它可安排在給定延遲後執行命令或者定期的執行。
11 ScheduledExecutorService Executors.newScheduledThreadPool(int corePoolSize)

  ExecutorService:

 1 // 執行任務/命令,沒有返回值,一般用來執行Runnable
 2 void execute(Runnable command)
 3 
 4 // 可用於提交一個Runnable,但沒有返回值
 5 Future<?> submit(Runnable task)
 6 
 7 // 執行任務,有返回值,一般用來執行Callable
 8 <T> Future<T> submit(Callable<T> task)
 9 
10 // 關閉連線池
11 void shutdown()

4、使用舉例

  程式碼示例:建立固定 5 個執行緒的執行緒池為 10 個任務服務。

 1 public class ThreadPoolTest {
 2     public static void main(String[] args) {
 3         // 1.建立一個 10 個執行緒數的執行緒池
 4         ExecutorService service = Executors.newFixedThreadPool(5);
 5 
 6         try {
 7             for (int i = 0; i < 10; i++) {
 8                 int finalI = i;
 9                 service.execute(() -> {
10 
11                     System.out.println(Thread.currentThread().getName() + " 為客戶 " + finalI + " 辦理業務~");
12 
13 //                    try {
14 //                        Thread.sleep(1000_000);
15 //                    } catch (InterruptedException e) {
16 //                        e.printStackTrace();
17 //                    }
18 
19                 });
20             }
21         } finally {
22             service.shutdown();
23         }
24     }
25 }
26 
27 // 可能的一種結果
28 pool-1-thread-1 為客戶 0 辦理業務~
29 pool-1-thread-2 為客戶 1 辦理業務~
30 pool-1-thread-2 為客戶 6 辦理業務~
31 pool-1-thread-2 為客戶 7 辦理業務~
32 pool-1-thread-2 為客戶 8 辦理業務~
33 pool-1-thread-1 為客戶 5 辦理業務~
34 pool-1-thread-2 為客戶 9 辦理業務~
35 pool-1-thread-3 為客戶 2 辦理業務~
36 pool-1-thread-4 為客戶 3 辦理業務~
37 pool-1-thread-5 為客戶 4 辦理業務~

  可以看到,銀行 5 個視窗為 10 個客戶相繼服務。若前面服務時間長(開啟註釋),執行緒池便沒有新的執行緒來執行任務了。程式會陷入等待中。
  程式碼示例:建立單個執行緒的執行緒池為 10 個執行緒服務。程式碼同上,只修改:

1 ExecutorService service = Executors.newSingleThreadExecutor();

  程式碼示例:建立可擴容執行緒的執行緒池為 10 個執行緒服務。程式碼同上,只修改:

1 ExecutorService service = Executors.newCachedThreadPool();

5、執行緒池好處

  為什麼要用執行緒池管理執行緒呢?當然是為了執行緒複用。
  背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
  思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁的建立和銷燬,實現重複利用。類似生活中的公共交通工具。
  好處:提高響應速度(減少了建立新執行緒的時間);降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立);便於執行緒管理。

二、執行緒池設計與實現

1、介紹

  前面介紹了三種(固定數、單一的、可變的)建立執行緒池的方式,實際工作用哪一個呢?都不使用!為什麼呢?
  《阿里巴巴Java開發手冊》明確規定:執行緒池不允許使用Executors建立,而是通過ThreadPoolExecutor的方式,規避資源耗盡風險。

  檢視原始碼,可以看到,用Executors建立執行緒池的三種方式中,都 new 了一個 ThreadPoolExecutor。所以,實際生產一般通過 ThreadPoolExecutor 的 7 個引數,自定義執行緒池。
  原始碼示例:7 個引數的構造器。

 1 public ThreadPoolExecutor(int corePoolSize,
 2                           int maximumPoolSize,
 3                           long keepAliveTime,
 4                           TimeUnit unit,
 5                           BlockingQueue<Runnable> workQueue,
 6                           ThreadFactory threadFactory,
 7                           RejectedExecutionHandler handler) {
 8     if (corePoolSize < 0 ||
 9         maximumPoolSize <= 0 ||
10         maximumPoolSize < corePoolSize ||
11         keepAliveTime < 0)
12         throw new IllegalArgumentException();
13     if (workQueue == null || threadFactory == null || handler == null)
14         throw new NullPointerException();
15     this.corePoolSize = corePoolSize;
16     this.maximumPoolSize = maximumPoolSize;
17     this.workQueue = workQueue;
18     this.keepAliveTime = unit.toNanos(keepAliveTime);
19     this.threadFactory = threadFactory;
20     this.handler = handler;
21 }

2、銀行服務

  介紹執行緒池之前,先來看一個生活中的案例。銀行業務辦理流程,如圖:

  某銀行一共有 5 個服務視窗,但平時一般只開放兩個,另外三個不開放。大廳中還有 10 個等待服務的座位。某天:
  (1)客人1(用Thread1表示)來辦理業務,他就直接去開放的視窗1辦理(假設他需要服務的時間很長,一直在服務中,後面的也一樣)。
  (2)Thread2來辦理業務,由於視窗1在服務中,所以他去了開放的視窗2辦理。
  (3)Thread3來辦理業務,由於視窗1和視窗2都在服務中,所以他去了大廳的等待服務座位上排隊等待。
  (4)Thread4~Thread12 同理Thread3。
  (5)Thread13來辦理業務,由於視窗1和視窗2都在服務中,且此時大廳的等待座位上也已滿。銀行經理便將關閉的視窗3開啟來為Thread13服務。注意:這裡並不是Thread13去大廳排隊,然後佇列中隊頭元素Thread3出隊接受服務。而是直接為Thread13服務。
  (6)Thread14,Thread15來辦理業務,會開放視窗4為Thread14服務,開放視窗5為Thread15服務。
  (7)Thread16來辦理業務,此時,已無可用視窗,且大廳的等待座位上也已滿。銀行便拒絕再為 Thread16 服務。
  說明:若 Thread13、Thread14、Thread15 業務辦理完畢後,沒有新的客人來銀行辦理業務。那麼視窗3、視窗4、視窗5會在一定時間後又關閉起來。

3、核心引數(重要)

  下面介紹 ThreadPoolExecutor 構造器中的 7 個核心引數。

  corePoolSize:執行緒池的核心執行緒數。
  maximumPoolSize:執行緒池的最大執行緒數,要大於corePoolSize。
  keepAliveTime:非核心執行緒閒置下來最多存活的時間。
  unit:執行緒池中非核心執行緒保持存活的時間單位,與keepAliveTime一起使用。
  workQueue:用來儲存提交後,等待執行任務的阻塞佇列。
  threadFactory:建立執行緒的工廠類。
  handler:拒絕策略。

  在理解上一節"銀行服務"的過程後,就不難理解上面 7 個引數的含義。

  corePoolSize = 2:視窗1 + 視窗2。
  maximumPoolSize = 5:視窗1 + 視窗2 + 視窗3 + 視窗4 + 視窗5。
  workQueue = 10:銀行大廳排隊佇列的大小。關於阻塞佇列 BlockingQueue<Runnable> workQueue 請看這篇。
  keepAliveTime + unit:"視窗3、視窗4、視窗5會在一定時間後又關閉起來"的時間。
  handler:"銀行便拒絕再為 Thread16 服務"的拒絕方式。

  在瞭解 ThreadPoolExecutor 7個核心引數的作用後,再看Executors建立的三種執行緒池的原始碼,就不難理解他們的作用。也就明白為什麼《阿里巴巴Java開發手冊》中禁止使用Executors建立,而要使用ThreadPoolExecutor自定義執行緒池。
  原始碼示例:
  一池N執行緒:建立一個固定(可重用)執行緒數的執行緒池。

 1 ExecutorService Executors.newFixedThreadPool(int nThreads);
 2 
 3 public static ExecutorService newFixedThreadPool(int nThreads) {
 4     return new ThreadPoolExecutor(nThreads, nThreads,
 5                                   0L, TimeUnit.MILLISECONDS,
 6                                   new LinkedBlockingQueue<Runnable>());
 7 }
 8 
 9 public LinkedBlockingQueue() {
10     this(Integer.MAX_VALUE);
11 }

  一池一執行緒:建立一個只有一個執行緒的執行緒池。

 1 ExecutorService Executors.newSingleThreadExecutor();
 2 
 3 public static ExecutorService newSingleThreadExecutor() {
 4     return new FinalizableDelegatedExecutorService
 5         (new ThreadPoolExecutor(1, 1,
 6                                 0L, TimeUnit.MILLISECONDS,
 7                                 new LinkedBlockingQueue<Runnable>()));
 8 }
 9 
10 public LinkedBlockingQueue() {
11     this(Integer.MAX_VALUE);
12 }

  可擴容:建立一個可根據需要執行緒數,建立新的執行緒的執行緒池。

1 ExecutorService Executors.newCachedThreadPool();
2 
3 public static ExecutorService newCachedThreadPool() {
4     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
5                                   60L, TimeUnit.SECONDS,
6                                   new SynchronousQueue<Runnable>());
7 }

4、工作流程(重要)

  在理解"銀行服務"的過程後,其實也就說清楚了執行緒池的工作流程。只是一些細節沒有說,比如:
  (1)視窗3為Thread13服務完成後,Thread14才來,情況如何?
  (2)……
  程式碼示例:銀行 2+3 個視窗為陸續來的 16 個客戶服務。

 1 public class ThreadPoolDemo {
 2     public static void main(String[] args) throws Exception {
 3         // 模擬上圖中 2 + 3 + 10(大廳排隊長度) 的執行緒池
 4         ThreadPoolExecutor executor = new ThreadPoolExecutor(
 5                 2, 5
 6                 , 6, TimeUnit.SECONDS
 7                 , new ArrayBlockingQueue<>(10)
 8                 , Executors.defaultThreadFactory()
 9                 , new ThreadPoolExecutor.AbortPolicy());
10 
11         // 建立16個執行緒模擬16個客戶
12         final int num = 16;
13         for (int i = 1; i <= num; i++) {
14             int finalI = i;
15 
16             // 這裡主要為了給執行緒起名字.
17             Thread thread = new Thread(() -> {
18                 System.out.println(Thread.currentThread().getName() + "=====" + finalI + " 號客人開始服務");
19 
20                 // 假設 13 和 16 服務很快.
21                 if (finalI != 16 && finalI != 13) {
22                     try {
23                         // 正在為 finalI 號客戶服務中
24                         Thread.sleep(1000_000);
25                     } catch (InterruptedException e) {
26                         e.printStackTrace();
27                     }
28                 }
29 
30                 System.out.println(Thread.currentThread().getName() + "=====" + finalI + " 號客人服務結束");
31             }, "------" + i);
32 
33             System.out.println(thread.getName() + " 號客人來了");
34             executor.execute(thread);
35 
36             // 讓主執行緒休息一下,保證上面開啟的執行緒先執行.
37             Thread.sleep(200);
38         }
39 
40         // 保證上面的執行緒執行完
41         Thread.sleep(1_000);
42 
43         System.out.println("===核心執行緒數===" + executor.getCorePoolSize());
44         System.out.println("===總任務數=====" + executor.getTaskCount());
45 
46         final BlockingQueue<Runnable> queue = executor.getQueue();
47         System.out.println("===正在排隊====" + queue.size());
48 
49         System.out.println("===最大執行緒數===" + executor.getMaximumPoolSize());
50         System.out.println("==============" + executor.getPoolSize());
51         System.out.println("===完成任務數===" + executor.getCompletedTaskCount());
52         System.out.println("==============" + executor.getLargestPoolSize());
53 
54         executor.shutdown();
55     }
56 }
57 
58 // 結果(程式未停止)
59 ------1 號客人來了
60 pool-1-thread-1=====1 號客人開始服務
61 ------2 號客人來了
62 pool-1-thread-2=====2 號客人開始服務
63 ------3 號客人來了
64 ------4 號客人來了
65 ------5 號客人來了
66 ------6 號客人來了
67 ------7 號客人來了
68 ------8 號客人來了
69 ------9 號客人來了
70 ------10 號客人來了
71 ------11 號客人來了
72 ------12 號客人來了 // 到這裡都不難理解
73 ------13 號客人來了
74 pool-1-thread-3=====13 號客人開始服務
75 pool-1-thread-3=====13 號客人服務結束 // 開放視窗3為客戶13立刻服務完畢.
76 pool-1-thread-3=====3 號客人開始服務 // 阻塞佇列,隊頭 客戶3 出隊接受視窗3的服務
77 ------14 號客人來了 // 加入阻塞佇列隊尾
78 ------15 號客人來了
79 pool-1-thread-4=====15 號客人開始服務
80 ------16 號客人來了
81 pool-1-thread-5=====16 號客人開始服務
82 pool-1-thread-5=====16 號客人服務結束
83 pool-1-thread-5=====4 號客人開始服務
84 ===核心執行緒數===2
85 ===總任務數=====16
86 ===正在排隊====9
87 ===最大執行緒數===5
88 ==============5
89 ===完成任務數===2
90 ==============5

  其他的情況,可通過修改程式碼示例中相關引數進行測試,自然就理解。

5、如何配置執行緒數

  執行緒在Java中屬於稀缺資源,執行緒池不是越大越好,也不是越小越好。那麼,執行緒池的引數要如何設定才合理呢?
  任務分為CPU密集型、IO密集型、混合型。
  CPU密集型:大部分都在用CPU跟記憶體,加密,邏輯操作,業務處理等。
  IO密集型:資料庫連結,網路通訊傳輸等。
  CPU密集型:一般推薦執行緒池不要過大,一般是CPU數 + 1,+1是因為可能存在頁缺失(就是可能存在有些資料在硬碟中需要多來一個執行緒將資料讀入記憶體)。如果執行緒池數太大,可能會頻繁的進行執行緒上下文切換跟任務排程。
  獲得當前CPU核心數程式碼如下:

1 Runtime.getRuntime().availableProcessors();

  IO密集型:執行緒數適當大一點,機器的CPU核心數*2。
  混合型:可以考慮根絕情況將它拆分成CPU密集型和IO密集型任務,如果執行時間相差不大,拆分可以提升吞吐量,反之沒有必要。

三、拒絕策略

1、介紹

  當執行緒池的任務快取佇列已滿並且執行緒池中的執行緒數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略,就會呼叫這個介面裡的這個方法。也就是"銀行便拒絕再為 Thread16 服務"的拒絕方式。

1 public interface RejectedExecutionHandler {
2     void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
3 }

2、4種拒絕策略

  ThreadPoolExecutor 提供了四種拒絕策略,分別是
  AbortPolicy:直接丟擲異常,這也是預設策略。
  CallerRunsPolicy:返回給呼叫者處理。用呼叫者所線上程來執行任務。
  DiscardOldestPolicy:拋棄佇列中等待最久的任務,然後把當前任務加入佇列中嘗試再次提交當前任務。
  DiscardPolicy:不處理,直接丟棄當前任務。

  程式碼示例:4種拒絕策略,程式碼同上,只需修改:

 1 // 1.去掉這個if條件
 2 if (finalI != 16 && finalI != 13) {}
 3 
 4 
 5 
 6 // 2.1 執行緒池建立時拒絕策略為:
 7 new ThreadPoolExecutor.AbortPolicy()
 8 // 2.1 結果,直接丟擲了異常.(只擷取了最後一點)
 9 ------16 號客人來了
10 Exception in thread "main" java.util.concurrent.RejectedExecutionException
11 
12 
13 
14 // 2.2 執行緒池建立時拒絕策略為:
15 new ThreadPoolExecutor.CallerRunsPolicy()
16 // 2.2 結果,返回給呼叫者main處理了.(只擷取了最後一點)
17 ------16 號客人來了
18 main=====16 號客人開始服務
19 
20 
21 
22 // 2.3 執行緒池建立時拒絕策略為:
23 new ThreadPoolExecutor.DiscardOldestPolicy()
24 // 2.3 結果,見下圖
25 
26 
27 
28 // 2.4 執行緒池建立時拒絕策略為:
29 new ThreadPoolExecutor.DiscardPolicy()
30 // 2.4 結果(丟棄了任務,沒有任何處理)

 

  通過debug斷點的方式,可以檢視到:DiscardOldestPolicy策略中,此時阻塞佇列中是客戶4~客戶16。也就是客戶3 出隊,被拋棄,客戶16入隊等待。

3、自定義拒絕策略

  如果不使用執行緒池提供的4種拒絕策略,也可以自己實現拒絕策略的介面,實現對這些超出數量的任務的處理。比如:為被拒絕的任務開啟一個新的執行緒執行,如下。

 1 // 執行緒池建立時拒絕策略為:
 2 new MyRejectedExecutionHandler()
 3 // 結果
 4 ------16 號客人來了
 5 ---開啟新執行緒處理任務---=====16 號客人開始服務
 6 
 7 
 8 // 自定義的策略拒絕
 9 class MyRejectedExecutionHandler implements RejectedExecutionHandler {
10 
11     @Override
12     public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
13         new Thread(r, "---開啟新執行緒處理任務---").start();
14     }
15 }

  參考文件:https://www.matools.com/api/java8

  《阿里巴巴Java開發手冊》百度網盤:https://pan.baidu.com/s/1FZ9DNr0sF1mAc6Nq_6QZmg    密碼: 4sw1

相關文章