1.3 Java 執行緒池
Java 執行緒池(Thread Pool)是一種執行緒的使用模式,是一種 Java 併發程式設計機制。Java 執行緒池能夠有效地管理執行緒,透過執行緒複用提升使用效率。當我們使用的執行緒一旦變多,特別在進行高效能測試時,執行緒池就是我們唯一的選擇。
使用執行緒池在以下幾個方面有著巨大優勢:
- (1)執行緒建立和銷燬:建立和銷燬是非常昂貴的操作,執行緒池透過複用已經建立的執行緒,減少執行緒的建立和銷燬的次數,提升了 Java 程式的效能,減少資源消耗。
- (2)執行緒管理:使用執行緒池可以處理不同的負載的任務,需要一種靈活且可靠的執行緒管理策略。首先我們需要把執行緒總數限制在一個合理的範圍內,其次要根據負載搞低、任務特性,及時增加或減少使用的執行緒,並且能夠及時回收不用的執行緒。執行緒池提供執行緒管理策略的模板,用以滿足不同的需求場景。
- (3)提升可維護性:使用執行緒池可以將執行緒建立和管理細節隱藏,只暴露提交任務的 API,是程式碼更加便於維護。讓測試工程師更加關注用例的細節,而不用過多關注多執行緒實現的細節。
Java 執行緒池提供了一種高效的方式實現多執行緒程式設計,解決了多執行緒使用中的各種問題,從而讓效能測試更加穩定、可靠。透過使用執行緒池,效能測試人員可以更加利用多核機器 CPU 效能,編寫更加高效能且安全穩定的測試用例。
1.3.1 Executors 建立執行緒池
java.util.concurrent.Executors 是 Java 標準庫中非常重要的工具類,經常會用來建立幾種常用型別的執行緒池。它提供了簡單的建立方法,對於剛剛入門 Java 多執行緒程式設計的讀者來說,透過 java.util.concurrent.Executors 提供的模板建立執行緒池是非常合適的選擇。
在效能測試中,我們通常會用到 2 類 Java 執行緒池:固定執行緒執行緒池(Fixed Thread Pool)和快取執行緒池(Cached Thread Pool)。
下面讓我們來了解這兩種執行緒池使用以及簡單應用。
1.固定執行緒執行緒池
固定執行緒執行緒池呼叫建立的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
根據 Java doc 描述:改方法建立一個執行緒池,擁有固定的執行緒數、無邊界(實際為 integer 最大值)共享任務佇列。改執行緒池限制了最大執行的執行緒數,如果某個執行緒執行過程中因為異常導致終止,當有新的任務需要執行時,會有建立新的執行緒。關閉該執行緒池需要呼叫java.util.concurrent.ExecutorService#shutdown
方法。
這個方法只有一個 int 引數 nThread,也就是執行緒池執行緒數。如果我們想使用該方法建立執行緒池,只需要考慮使用場景到底需要多少執行緒即可。使用方法如下:
ExecutorService executorService = Executors.newFixedThreadPool(3);// 建立執行緒數為3的執行緒池
然後就是往執行緒池中提交任務,java.util.concurrent.ExecutorService 提供了 2 個方法用於往執行緒池提交任務。其中效能測試中最常用的是 java.util.concurrent.Executor#execute,引數型別 java.lang.Runnable。還有 java.util.concurrent.ExecutorService#submit() 方法,有三個過載方法,在效能測試中極少用到,通常自動化測試專案用的比較多,本章不再展開講解。
固定執行緒執行緒池演示程式碼如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}實現Java 固定執行緒執行緒池
*/
public class FixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);// 建立執行緒數為3的執行緒池
for (int i = 0; i < 4; i++) {// 迴圈4次
executorService.execute(new Runnable() {// 提交任務
@Override
public void run() {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 列印當前時間、執行緒名稱
}
});
}
executorService.shutdown();// 關閉執行緒池
}
}
控制檯輸出:
1695555879091 Hello FunTester! pool-1-thread-3
1695555879091 Hello FunTester! pool-1-thread-1
1695555879091 Hello FunTester! pool-1-thread-2
1695555879194 Hello FunTester! pool-1-thread-3
程序已結束,退出程式碼為 0。
當然我們還可以使用 Lambda 語法使程式碼更加簡潔:
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());//
});
我們看到前 3 個資訊是在同一時間列印的,且執行緒均不相同。第四條資訊列印時間戳比前三條多了 103ms,這與程式碼中 Thread.sleep(100);休眠的 100 毫秒是基本一致的。這裡顯示我們建立的執行緒池中 3 個執行緒,當在執行任務的前 100 毫秒在處理前 3 個任務,等到某個任務結束後,再執行第 4 個任務。
我們可以得出一個結論:當固定執行緒執行緒池的執行緒都在處理別的任務(繁忙狀態),剩餘的任務實在等待執行的過程中,實際上任務是再等待佇列中。在實際使用場景中,限制執行緒池中最大的執行緒數會導致執行緒池有一個處理任務的上限。
固定執行緒執行緒池適用於具有固定數量、需要嚴格控制最大執行緒數的併發執行場景。固定執行緒執行緒池能夠提供穩定的執行緒數量,使任務執行的速率更加均勻。在使用過程中,需要注意任務佇列中的等待數量,防止超量積壓導致任務從提交到最終執行延遲過大。
在效能測試中,通常使用固定執行緒執行緒池來執行執行緒模型中固定執行緒、需要最大執行緒數限制的動態執行緒場景。
2.快取執行緒池
首先我們觀察快取執行緒池的建立方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
根據 Java doc 的描述:改執行緒池根據需要建立執行緒,但是會複用已經建立好的執行緒,適合執行大量的短期的任務。如果執行緒空閒超過設定 60 秒,則會被回收,若是長時間未使用,該執行緒池會回收所有執行緒資源。
這是一個無參的方法,但不意味著我們可以無限建立執行緒,該方法建立的快取執行緒池最大可以建立java.lang.Integer#MAX_VALUE
個執行緒,但在實際的使用中,優先於系統限制和資源限制,能夠建立的執行緒數遠低於這個值。
線上程池的使用方面,快取執行緒池是完全跟固定執行緒執行緒池一樣的,有兩種提交任務的方法,這裡我們展示 java.util.concurrent.Executor#execute 方法,下面是演示 Demo:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}實現Java 快取執行緒池
*/
public class CachedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 建立一個快取執行緒池
for (int i = 0; i < 8; i++) {// 迴圈8次
// 提交任務
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 輸出當前時間和執行緒名
});
}
executorService.shutdown();// 關閉執行緒池
}
}
下面是控制檯輸出:
1696727545116 Hello FunTester! pool-1-thread-6
1696727545116 Hello FunTester! pool-1-thread-1
1696727545116 Hello FunTester! pool-1-thread-3
1696727545116 Hello FunTester! pool-1-thread-2
1696727545116 Hello FunTester! pool-1-thread-7
1696727545116 Hello FunTester! pool-1-thread-4
1696727545116 Hello FunTester! pool-1-thread-8
1696727545116 Hello FunTester! pool-1-thread-5
程序已結束,退出程式碼為 0。
使用 Lambda 語法簡化程式碼同固定執行緒執行緒池,這裡不再展示。
可以看出,我們提交了 8 個任務到執行緒池,幾乎在同一時刻(時間戳相同)任務均被執行,且執行的執行緒都不一樣。快取執行緒池總計建立了 8 個執行緒,分別執行不同的任務。
在下面例子中,我們設計了每個執行緒任務在提交之前均休眠不同的時間,這樣可以很清晰展示快取執行緒池執行緒複用的效果。演示程式碼如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}實現Java 快取執行緒池
*/
public class CachedThreadPoolReuseDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 建立一個快取執行緒池
for (int i = 0; i < 8; i++) {//
try {
Thread.sleep(i * 100);// 睡眠i*100毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 提交任務
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 輸出當前時間和執行緒名
});
}
executorService.shutdown();// 關閉執行緒池
}
}
控制檯輸出:
1696728595770 Hello FunTester! pool-1-thread-1
1696728595873 Hello FunTester! pool-1-thread-2
1696728596074 Hello FunTester! pool-1-thread-2
1696728596376 Hello FunTester! pool-1-thread-2
1696728596777 Hello FunTester! pool-1-thread-2
1696728597281 Hello FunTester! pool-1-thread-2
1696728597880 Hello FunTester! pool-1-thread-2
1696728598580 Hello FunTester! pool-1-thread-2
程序已結束,退出程式碼為 0
可以看到快取執行緒池總計建立了 2 個執行緒用來執行 8 個任務,因為後來的任務到達時,前一個任務已經執行結束,執行緒已經空閒下來,可以執行新的任務。
接下來透過下面的例子演示快取執行緒池使用中,當執行緒被異常終止時,執行緒池如何處理。演示程式碼如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}實現Java 快取執行緒池
*/
public class CachedThreadPoolAbortDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 建立一個快取執行緒池
for (int i = 0; i < 2; i++) {//
try {
Thread.sleep(i * 150);// 睡眠i*100毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 提交任務
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 輸出當前時間和執行緒名
throw new RuntimeException("執行緒異常");
});
}
executorService.shutdown();// 關閉執行緒池
}
}
控制檯輸出:
1696729397014 Hello FunTester! pool-1-thread-1
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 執行緒異常
at org.funtester.performance.books.chapter01.section3.CachedThreadPoolAbortDemo.lambda$main$0(CachedThreadPoolAbortDemo.java:27)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
1696729397165 Hello FunTester! pool-1-thread-2
Exception in thread "pool-1-thread-2" java.lang.RuntimeException: 執行緒異常
at org.funtester.performance.books.chapter01.section3.CachedThreadPoolAbortDemo.lambda$main$0(CachedThreadPoolAbortDemo.java:27)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
程序已結束,退出程式碼為 0。
我們先忽略這兩個報錯資訊,可以看到執行緒池總計建立了 2 個執行緒去執行 2 個任務。根據我們之前的所學內容,如果不報錯的話,只需要建立 1 個執行緒即可以執行這 2 個任務。這也對應了 Java doc 描中,當執行緒池終的執行緒被終止時,若仍需要執行緒,就會建立新的執行緒執行任務。這個邏輯不僅使用快取執行緒池,也適用於固定執行緒執行緒池,包括 1.4 節的自定義執行緒池。
FunTester 原創精華
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片