在之前的文章Java面試知識點(七十三)線程池 ,已經說了線程池的基本情況,包括線程池的運行原理,線程池的創建,任務的提交,獲取結果,線程池的關閉和配置,下面我們在深入的瞭解一下線程池並進行代碼編寫。
一、線程池的繼承架構
Java 裏面線程池的頂級接口是 Executor,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具。
真正的線程池接口是 ExecutorService。下面這張圖完整描述了線程池的類體系結構。
-
Executor 是一個頂層接口(類似一個標記接口),在它裏面只聲明瞭一個方法 execute (Runnable),返回值爲 void,參數爲 Runnable 類型,從字面意思可以理解,就是用來執行傳進去的任務的;
-
然後 ExecutorService 接口繼承了 Executor 接口,並聲明瞭一些方法:submit、invokeAll、invokeAny 以及 shutDown 等;
-
抽象類 AbstractExecutorService 實現了 ExecutorService 接口,基本實現了 ExecutorService 中聲明的所有方法;
-
ThreadPoolExecutor 繼承了類 AbstractExecutorService。
整理得:
二、線程池相關類
1.Executors 類
該類裏面提供了一些靜態工廠,生成一些常用的線程池。
-
newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行,使用的阻塞隊列是LinkedBlockingQueue;
-
newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程,使用的 LinkedBlockingQueue;
-
newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60 秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。 此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說 JVM)能夠創建的最大線程大小,newCachedThreadPool 將 corePoolSize 設置爲 0,將 maximumPoolSize 設置爲 Integer.MAX_VALUE,使用的 SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過 60 秒,就銷燬線程。
-
newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
-
newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。
-
【源碼】
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
注意:聲明LinkedBlockingQueue的時候,可以指定大小,也可以不指定,不指定的時候,就是默認Integer.MAX_VALUE的大小,所以當阻塞隊列是linked並且不指定大小的時候,提交任務是否溢出是根據內存來確定的。而arrayblockingqueue必須指定大小
2.Future類
-
Future 表示異步計算的結果。
它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。
計算完成後只能使用 get 方法來獲取結果,如有必要,計算完成前可以阻塞此方法。 -
取消則由 cancel 方法來執行。還提供了其他方法,以確定任務是正常完成還是被取消了。一旦計算完成,就不能再取消計算。
-
如果爲了可取消性而使用 Future 但又不提供可用的結果,則可以聲明 Future<?> 形式類型、並返回 null 作爲底層任務的結果。
-
Future 就是對於具體的 Runnable 或者 Callable 任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過 get 方法獲取執行結果,該方法會阻塞直到任務返回結果。
-
也就是說 Future 提供了三種功能:
-
判斷任務是否完成;
-
能夠中斷任務;
-
能夠獲取任務執行結果。
-
-
Future 類的方法
-
boolean cancel (boolean mayInterruptIfRunning) 試圖取消對此任務的執行。
-
V get () 如有必要,等待計算完成,然後獲取其結果。
-
V get (long timeout, TimeUnit unit) 如有必要,最多等待爲使計算完成所給定的時間之後,獲取其結果(如果結果可用)。
-
boolean isCancelled () 如果在任務正常完成前將其取消,則返回 true。
-
boolean isDone () 如果任務已完成,則返回 true。
-
三、代碼示例
1.無返回值的Runable示例
【實現runnable接口的線程類】
package test.threadpool;
public class Water implements Runnable {
private int num;
public Water(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println("第" + num + "號選手入場");
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + num + "號選手黯然離場");
}
}
【生產環境】
package test.threadpool;
import java.util.concurrent.*;
public class Produce {
public static void main(String[] args) {
// 核心線程數
int corePoolSize =5;
// 線程池總大小
int poolSize = 10;
// 任務阻塞隊列
BlockingQueue<Runnable> queue = new ArrayBlockingQueue(5);
// 創建線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
poolSize,
2000,
TimeUnit.MICROSECONDS,
queue);
// 注意這裏的循環次數,當超過池和阻塞隊列的和之後,線程池會拒絕,使用默認的拒絕策略,即拋出異常
for (int i=1; i<16; i++) {
Water task = new Water(i);
executor.execute(task);
System.out.println("線程池中當前線程數:"+executor.getPoolSize()+
",等待執行的任務數:"+executor.getQueue().size()+
",已經完成人數數:"+executor.getCompletedTaskCount());
}
// 關閉線程池
executor.shutdown();
}
}
【運行結果(不唯一)】
線程池中線程數:1,等待執行的任務數:0,已經完成人數數:0
第1號選手入場
線程池中線程數:2,等待執行的任務數:0,已經完成人數數:0
第2號選手入場
線程池中線程數:3,等待執行的任務數:0,已經完成人數數:0
第3號選手入場
線程池中線程數:4,等待執行的任務數:0,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:0,已經完成人數數:0
第4號選手入場
線程池中線程數:5,等待執行的任務數:1,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:2,已經完成人數數:0
第5號選手入場
線程池中線程數:5,等待執行的任務數:3,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:4,已經完成人數數:0
線程池中線程數:5,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:6,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:7,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:8,等待執行的任務數:5,已經完成人數數:0
第11號選手入場
第12號選手入場
線程池中線程數:9,等待執行的任務數:5,已經完成人數數:0
線程池中線程數:10,等待執行的任務數:5,已經完成人數數:0
第13號選手入場
第14號選手入場
第15號選手入場
第3號選手黯然離場
第14號選手黯然離場
第15號選手黯然離場
第1號選手黯然離場
第12號選手黯然離場
第5號選手黯然離場
第2號選手黯然離場
第11號選手黯然離場
第13號選手黯然離場
第4號選手黯然離場
第10號選手入場
第9號選手入場
第8號選手入場
第7號選手入場
第6號選手入場
第9號選手黯然離場
第7號選手黯然離場
第10號選手黯然離場
第8號選手黯然離場
第6號選手黯然離場
從執行結果可以看出,當線程池中線程的數目大於 5 時,便將任務放入任務緩存隊列裏面,當任務緩存隊列滿了之後,便創建新的線程。
如果上面程序中,將 for 循環中改成執行 20 個任務,就會拋出任務拒絕異常了。
注意:聲明LinkedBlockingQueue的時候,可以指定大小,也可以不指定,不指定的時候,就是默認Integer.MAX_VALUE的大小,所以當阻塞隊列是linked並且不指定大小的時候,提交任務是否溢出是根據內存來確定的。而arrayblockingqueue必須指定大小
2.有返回值的Callable示例
callable也是實現多線程的一種方式,但是單獨用的情況很少,大部分情況是和線程池一起使用
Runnable 和 Callable 的區別
- Runnable 執行方法是 run (),Callable 是 call ()
- 實現 Runnable 接口的任務線程無返回值;實現 Callable 接口的任務線程能返回執行結果
- call 方法可以拋出異常,run 方法若有異常只能在內部消化
【實現callable接口】
package test.threadpool;
import java.util.concurrent.Callable;
public class CallabelImpl implements Callable {
private int num;
public CallabelImpl(int num) {
this.num = num;
}
@Override
public Integer call() throws Exception {
int result = 0;
for (int i=0; i<=num; i++) {
result += i;
System.out.println("這是:"+num+" 的計算進度:"+result);
}
System.out.println("<<<<<<<<計算結束>>>>>>>");
return result;
}
}
【生產環境】
package test.threadpool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Demo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Integer> f1 = pool.submit(new CallabelImpl(5));
Future<Integer> f2 = pool.submit(new CallabelImpl(10));
try {
Integer i1 = f1.get();
Integer i2 = f2.get();
System.out.println(i1+"----"+i2);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
}
pool.shutdown();
}
}
【運行結果(線程池大小爲1,結果唯一)】
這是:5 的計算進度:0
這是:5 的計算進度:1
這是:5 的計算進度:3
這是:5 的計算進度:6
這是:5 的計算進度:10
這是:5 的計算進度:15
<<<<<<<<計算結束>>>>>>>
這是:10 的計算進度:0
這是:10 的計算進度:1
這是:10 的計算進度:3
這是:10 的計算進度:6
這是:10 的計算進度:10
這是:10 的計算進度:15
這是:10 的計算進度:21
這是:10 的計算進度:28
這是:10 的計算進度:36
這是:10 的計算進度:45
這是:10 的計算進度:55
<<<<<<<<計算結束>>>>>>>
15----55
但是如果,把線程池的大小設置成大於等於2
ExecutorService pool = Executors.newFixedThreadPool(2);
【結果(不唯一)】
這是:10 的計算進度:0
這是:5 的計算進度:0
這是:10 的計算進度:1
這是:10 的計算進度:3
這是:10 的計算進度:6
這是:10 的計算進度:10
這是:5 的計算進度:1
這是:10 的計算進度:15
這是:10 的計算進度:21
這是:10 的計算進度:28
這是:10 的計算進度:36
這是:10 的計算進度:45
這是:10 的計算進度:55
<<<<<<<<計算結束>>>>>>>
這是:5 的計算進度:3
這是:5 的計算進度:6
這是:5 的計算進度:10
這是:5 的計算進度:15
<<<<<<<<計算結束>>>>>>>
15----55
分析:當線程池只有一個的時候,提交兩個任務,一個在corepool中一個在阻塞隊列中,在阻塞隊列中的任務會等待池中的任務運行結束,在進入池中開始執行。