線程池介紹
線程池(Thread Pool):把一個或多個線程通過統一的方式進行調度和重複使用的技術,避免了因爲線程過多而帶來使用上的開銷。
爲什麼要使用線程池?
-
可重複使用已有線程,避免對象創建、消亡和過度切換的性能開銷。
-
避免創建大量同類線程所導致的資源過度競爭和內存溢出的問題。
-
支持更多功能,比如延遲任務線程池(newScheduledThreadPool)和緩存線程池(newCachedThreadPool)等。
線程池使用
創建線程池有兩種方式:ThreadPoolExecutor 和 Executors,其中 Executors 又可以創建 6 種不同的線程池類型,會在下節講,本節重點來看看 ThreadPoolExecutor 的使用。
ThreadPoolExecutor 的使用
線程池使用代碼如下:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
// 執行線程池
System.out.println("Hello, Java.");
}
});
以上程序執行結果如下:
Hello, Java.
ThreadPoolExecutor 參數說明
ThreadPoolExecutor 構造方法有以下四個,如下圖所示:
其中最後一個構造方法有 7 個構造參數,包含了前三個方法的構造參數,這 7 個參數名稱如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//...
}
其代表的含義如下:
① corePoolSize
線程池中的核心線程數,默認情況下核心線程一直存活在線程池中,如果將 ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設爲 true,如果線程池一直閒置並超過了 keepAliveTime 所指定的時間,核心線程就會被終止。
② maximumPoolSize
線程池中最大線程數,如果活動的線程達到這個數值以後,後續的新任務將會被阻塞(放入任務隊列)。
③ keepAliveTime
線程池的閒置超時時間,默認情況下對非核心線程生效,如果閒置時間超過這個時間,非核心線程就會被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 設爲 true 的時候,核心線程如果超過閒置時長也會被回收。
④ unit
配合 keepAliveTime 使用,用來標識 keepAliveTime 的時間單位。
⑤ workQueue
線程池中的任務隊列,使用 execute() 或 submit() 方法提交的任務都會存儲在此隊列中。
⑥ threadFactory
爲線程池提供創建新線程的線程工廠。
⑦ rejectedExecutionHandler
線程池任務隊列超過最大值之後的拒絕策略,RejectedExecutionHandler 是一個接口,裏面只有一個 rejectedExecution 方法,可在此方法內添加任務超出最大值的事件處理。ThreadPoolExecutor 也提供了 4 種默認的拒絕策略:
-
new ThreadPoolExecutor.DiscardPolicy():丟棄掉該任務,不進行處理
-
new ThreadPoolExecutor.DiscardOldestPolicy():丟棄隊列裏最近的一個任務,並執行當前任務
-
new ThreadPoolExecutor.AbortPolicy():直接拋出 RejectedExecutionException 異常
-
new ThreadPoolExecutor.CallerRunsPolicy():既不拋棄任務也不拋出異常,直接使用主線程來執行此任務
包含所有參數的 ThreadPoolExecutor 使用代碼:
public class ThreadPoolExecutorTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
class MyThreadFactory implements ThreadFactory {
private AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
String threadName = "MyThread" + count.addAndGet(1);
t.setName(threadName);
return t;
}
}
線程池執行方法 execute() VS submit()
execute() 和 submit() 都是用來執行線程池的,區別在於 submit() 方法可以接收線程池執行的返回值。
下面分別來看兩個方法的具體使用和區別:
// 創建線程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 使用
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello, Java.");
}
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("Hello, 老王.");
return "Success";
}
});
System.out.println(future.get());
以上程序執行結果如下:
Hello, Java.
Hello, 老王.
Success
線程池關閉
線程池關閉,可以使用 shutdown() 或 shutdownNow() 方法,它們的區別是:
-
shutdown():不會立即終止線程池,而是要等所有任務隊列中的任務都執行完後纔會終止。執行完 shutdown 方法之後,線程池就不會再接受新任務了。
-
shutdownNow():執行該方法,線程池的狀態立刻變成 STOP 狀態,並試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,執行此方法會返回未執行的任務。
下面用代碼來模擬 shutdown() 之後,給線程池添加任務,代碼如下:
threadPoolExecutor.execute(() -> {
for (int i = 0; i < 2; i++) {
System.out.println("I'm " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
});
threadPoolExecutor.shutdown();
threadPoolExecutor.execute(() -> {
System.out.println("I'm Java.");
});
以上程序執行結果如下:
I'm 0
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.interview.chapter5.Section2$$Lambda$2/1828972342@568db2f2 rejected from java.util.concurrent.ThreadPoolExecutor@378bf509[Shutting down, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
I'm 1
可以看出,shutdown() 之後就不會再接受新的任務了,不過之前的任務會被執行完成。
相關面試題
1.ThreadPoolExecutor 有哪些常用的方法?
答:常用方法如下所示:
-
submit()/execute():執行線程池
-
shutdown()/shutdownNow():終止線程池
-
isShutdown():判斷線程是否終止
-
getActiveCount():正在運行的線程數
-
getCorePoolSize():獲取核心線程數
-
getMaximumPoolSize():獲取最大線程數
-
getQueue():獲取線程池中的任務隊列
-
allowCoreThreadTimeOut(boolean):設置空閒時是否回收核心線程
2.以下程序執行的結果是什麼?
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("I:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadPoolExecutor.shutdownNow();
System.out.println("Java");
答:程序執行的結果是:
I:0
Java
java.lang.InterruptedException: sleep interrupted(報錯信息)
I:1
題目解析:因爲程序中使用了 shutdownNow()
會導致程序執行一次之後報錯,拋出 sleep interrupted
異常,又因爲本身有 try/catch,所以程序會繼續執行打印 I:1
。
3.在 ThreadPool 中 submit() 和 execute() 有什麼區別?
答:submit() 和 execute() 都是用來執行線程池的,只不過使用 execute() 執行線程池不能有返回方法,而使用 submit() 可以使用 Future 接收線程池執行的返回值。
submit() 方法源碼(JDK 8)如下:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
execute() 源碼(JDK 8)如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//..... 其他
}
4.說一下 ThreadPoolExecutor 都需要哪些參數?
答:ThreadPoolExecutor 最多包含以下七個參數:
-
corePoolSize:線程池中的核心線程數
-
maximumPoolSize:線程池中最大線程數
-
keepAliveTime:閒置超時時間
-
unit:keepAliveTime 超時時間的單位(時/分/秒等)
-
workQueue:線程池中的任務隊列
-
threadFactory:爲線程池提供創建新線程的線程工廠
-
rejectedExecutionHandler:線程池任務隊列超過最大值之後的拒絕策略
更多詳細介紹,請見正文。
5.在線程池中 shutdownNow() 和 shutdown() 有什麼區別?
答:shutdownNow() 和 shutdown() 都是用來終止線程池的,它們的區別是,使用 shutdown() 程序不會報錯,也不會立即終止線程,它會等待線程池中的緩存任務執行完之後再退出,執行了 shutdown() 之後就不能給線程池添加新任務了;shutdownNow() 會試圖立馬停止任務,如果線程池中還有緩存任務正在執行,則會拋出 java.lang.InterruptedException: sleep interrupted 異常。
6.說一說線程池的工作原理?
答:當線程池中有任務需要執行時,線程池會判斷如果線程數量沒有超過核心數量就會新建線程池進行任務執行,如果線程池中的線程數量已經超過核心線程數,這時候任務就會被放入任務隊列中排隊等待執行;如果任務隊列超過最大隊列數,並且線程池沒有達到最大線程數,就會新建線程來執行任務;如果超過了最大線程數,就會執行拒絕執行策略。
7.以下線程名稱被打印了幾次?
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new ThreadPoolExecutor.DiscardPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
// 打印線程名稱
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
答:線程名被打印了 3 次。
題目解析:線程池第 1 次執行任務時,會新創建任務並執行;第 2 次執行任務時,因爲沒有空閒線程所以會把任務放入隊列;第 3 次同樣把任務放入隊列,因爲隊列最多可以放兩條數據,所以第 4 次之後的執行都會被捨棄(沒有定義拒絕策略),於是就打印了 3 次線程名稱。
總結
ThreadPoolExecutor 是創建線程池最傳統和最推薦使用的方式,創建時要設置線程池的核心線程數和最大線程數還有任務隊列集合,如果任務量大於隊列的最大長度,線程池會先判斷當前線程數量是否已經到達最大線程數,如果沒有達到最大線程數就新建線程來執行任務,如果已經達到最大線程數,就會執行拒絕策略(拒絕策略可自行定義)。線程池可通過 submit() 來調用執行,從而獲得線程執行的結果,也可以通過 shutdown() 來終止線程池。
下一篇:Java 線程池
在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈
往期精選