線程池它就是一個池子(就像是養魚的池子),可以養一定數量的魚,可以重複使用!學習Java中的線程池,就是學習Java用了什麼工具(API)和方法(設計模式)來搞出可以“養魚的池子”。
本文作爲入門級的線程池教程,主要介紹第一個線程池的一般寫法,也就是“Hello,world”的水平,快速入門!
Table of Contents
什麼是線程池
背景:
- 如果反覆創建銷燬線程,也會有一定的系統資源開銷;
- 線程池,其實就是一個容納多個線程的容器;Java初始時提供一定數量的、可重複使用的線程;
- 省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。
- 此外,當我們線程創建過多時,容易引發內存溢出,因此我們就有必要使用線程池的技術了。
爲什麼要使用線程池?
- 在java中,如果每個請求到達就創建一個新線程,開銷是相當大的。
- 在實際使用中,創建和銷燬線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創建和銷燬線程的開銷之外,活動的線程也需要消耗系統資源。
- 如果在一個jvm裏創建太多的線程,可能會使系統由於過度消耗內存或“切換過度”而導致系統資源不足。
- 爲了防止資源不足,需要採取一些辦法來限制任何給定時刻處理的請求數目,儘可能減少創建和銷燬線程的次數,特別是一些資源耗費比較大的線程的創建和銷燬,儘量利用已有對象來進行服務。
- 線程池主要用來解決線程生命週期開銷問題和資源不足問題。
- 通過對多個任務重複使用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。
- 這樣,就可以立即爲請求服務,使用應用程序響應更快。
- 另外,通過適當的調整線程中的線程數目可以防止出現資源不足的情況。
《Java 併發編程的藝術》提到的來說一下使用線程池的好處:
- 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
注意:
- 程序一開始的時候,創建多個線程對象,存儲到集合中;當需要線程的時候,從幾回閤中獲取線程出來;
- 從JDK1.5開始,程序員不再需要自己開發線程池,而是使用內置線程池技術;
線程池體系
JDK給我們提供了Excutor框架來使用線程池,它是線程池的基礎。Executor提供了一種將“任務提交”與“任務執行”分離開來的機制(解耦)。
第一個線程池
線程池的使用流程:
- 調用Executors類的靜態方法創建線程池;
- 調用submit提交Runnable或Callable對象;
- 保存好返回的Future對象,以便得到結果或者取消任務;
- 當不想再提交任何任務時,調用shutdown
向線程池中提交Runnable接口的線程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo {
public static void main(String[] args) {
//第一步:調用Executors類的靜態方法創建線程池;
ExecutorService es = Executors.newFixedThreadPool(5); //創建一個固定大小的線程池
//第二步-1:Runnable或Callable對象
MyTask task = new MyTask();
//第二步-2:提交Runnable或Callable對象到線程池中
for(int i=0; i<10; i++) {
es.submit(task); //連續提交10個線程到線程池中
}
es.shutdown();
}
}
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("時間:"+System.currentTimeMillis()+";線程執行ID:"+
Thread.currentThread().getName());
try {
Thread.sleep(1000); //休眠1s
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
程序輸出:
- 時間:1585382666365;線程執行ID:pool-1-thread-1
- 時間:1585382666365;線程執行ID:pool-1-thread-4
- 時間:1585382666365;線程執行ID:pool-1-thread-5
- 時間:1585382666365;線程執行ID:pool-1-thread-2
- 時間:1585382666365;線程執行ID:pool-1-thread-3
- 時間:1585382667366;線程執行ID:pool-1-thread-5
- 時間:1585382667367;線程執行ID:pool-1-thread-1
- 時間:1585382667367;線程執行ID:pool-1-thread-2
- 時間:1585382667367;線程執行ID:pool-1-thread-3
- 時間:1585382667367;線程執行ID:pool-1-thread-4
分析:
- 根據輸出:前5個任務和後5個任務的執行時間正好相差1s!
- 並且,前5個和後5個任務的線程ID都是完全一致的!說明前5個線程是同一個整體,後5個線程又是另一個整體!
向線程池中提交Callable接口的線程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadDemo {
public static void main(String[] args) {
//第一步:調用Executors類的靜態方法創建線程池;
ExecutorService es = Executors.newFixedThreadPool(2); //創建一個固定大小的線程池
//第二步-1:創建Runnable或Callable對象
MyTask task = new MyTask();
//第二步-2:提交Runnable或Callable對象到線程池中
//第三步:用Future保存好每個任務,以便對任務的操作
Future<String> re01 = es.submit(task);
Future<String> re02 = es.submit(task);
try {
System.out.println(re01.get()+re02.get()); //對任務進行操作!
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//第四步:關閉線程池
es.shutdown();
}
}
class MyTask implements Callable<String> {
public String call() {
String info = Thread.currentThread().getName();
return info+"執行了....";
}
}
Callable接口和Runnable接口一樣,都是Java中多線程的實現方式。其中Callable接口實現的線程可以拋出異常,也可以有返回值,而Runnable接口實現的線程則不可以(不可拋異常,不可有返回值)!