并發設計的本質,就是要把程序的邏輯分解為多個任務,這些任務獨立而又協作的完成程序的功能。而其中最關鍵的地方就是如何將邏輯上的任務分配到實際的線程中去執行。換而言之,任務是目的,而線程是載體,線程的實現要以任務為目標。
1. 在線程中執行任務
并發程序設計的第一步就是要劃分任務的邊界,理想情況下就是所有的任務都獨立的:每個任務都是不依賴于其他任務的狀態,結果和邊界。因為獨立的任務是最有利于并發設計的。
有一種最自然的任務劃分方法就是以獨立的客戶請求為任務邊界。每個用戶請求是獨立的,則處理任務請求的任務也是獨立的。
在劃分完任務之后,下一問題就是如何調度這些任務,最簡單的方法就是串行調用所有任務,也就是一個一個的執行。
比如下面的這個套接字服務程序,每次都只能響應一個請求,下一個請求需要等上一個請求執行完畢之后再被處理。
public class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
Socket connection = socket.accept();
handleRequest(connection);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
這種設計當然是不能滿足要求的,并發的高吞吐和高響應速度的優勢都沒發揮出來。
1.1 顯示地創建線程
上述代碼的優化版就是為每個請求都分配獨立的線程來執行,也就是每一個請求任務都是一個獨立線程。
public class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
//為每個請求創建單獨的線程任務,保證并發性
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
new Thread(task).start();
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
這樣設計的優點在于:
- 任務處理線程從主線程分離出來,使得主線程不用等待任務完畢就可以去快速地去響應下一個請求,以達到高響應速度;
- 任務處理可以并行,支持同時處理多個請求;
- 任務處理是線程安全的,因為每個任務都是獨立的。
不過需要注意的是,任務必須是線程安全的,否者多線程并發時會有問題。
1.3 無限制創建線程的不足
但是以上的方案還是有不足的:
- 線程的生命周期的開銷很大:每創建一個線程都是要消耗大量的計算資源;
- 資源的消耗:活躍的線程要消耗內存資源,如果有太多的空閑資源就會使得很多內存資源浪費,導致內存資源不足,多線程并發時就會出現資源強占的問題;
- 穩定性:可創建線程的個數是有限制的,過多的線程數會造成內存溢出;
利用創建線程來攻擊的例子中,最顯而易見的就是不斷創建死循環的線程,最終導致整個計算機的資源都耗盡。
2.Executor框架
任務是一組邏輯工作單元,而線程則是任務異步執行的機制。為了讓任務更好地分配到線程中執行,java.util.concurrent提供了Executor框架。
Executor基于生產者-消費者模式:提交任務的操作相當于生產者(生成待完成的工作單元),執行任務的線程則相當于消費者(執行完這些工作單元)。
將以上的服務端代碼改造為Executor框架如下:
public class TaskExecutionWebServer {
//設定線程池大小;
private static final int NTHREADS = 100;
private static final Executor exec
= Executors.newFixedThreadPool(NTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
2.1 線程池
Executor的本質就是管理和調度線程池。所謂線程池就是指管理一組同構工作線程的資源池。線程池和任務隊列相輔相成:任務隊列中保存著所有帶執行的任務,而線程池中有著可以去執行任務的工作線程,工作線程從任務隊列中領域一個任務執行,執行任務完畢之后在回到線程池中等待下一個任務的到來。
任務池的優勢在于:
- 通過復用現有線程而不是創建新的線程,降低創建線程時的開銷;
- 復用現有線程,可以直接執行任務,避免因創建線程而讓任務等待,提高響應速度。
Executor可以創建的線程池共有四種:
- newFixedThreadPool,即固定大小的線程池,如果有線程因發生了異常而崩潰,會創建新的線程代替:
- newCachedThreadPool,即支持緩存的線程池,如果線程池的規模超過了需求的規模,就會回收空閑線程,如果需求增加,則會增加線程池的規模;
- newScheduledThreadPool,固定大小的線程池,而且以延時或者定時的方式執行;
- newSingleThreadExecutor,單線程模式,串行執行任務;
2.2 Executor的生命周期
這里需要單獨說下Executor的生命周期。由于JVM只有在非守護線程全部終止才會退出,所以如果沒正確退出Executor,就會導致JVM無法正常結束。但是Executor是采用異步的方式執行線程,并不能立刻知道所有線程的狀態。為了更好的管理Executor的生命周期,Java1.5開始提供了Executor的擴展接口ExecutorService。
ExecutorService提供了兩種方法關閉方法:
- shutdown: 平緩的關閉過程,即不再接受新的任務,等到已提交的任務執行完畢后關閉進程池;
- shutdownNow: 立刻關閉所有任務,無論是否再執行;
服務端ExecutorService版的實現如下:
public class LifecycleWebServer {
private final ExecutorService exec = Executors.newCachedThreadPool();
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while (!exec.isShutdown()) {
try {
final Socket conn = socket.accept();
exec.execute(new Runnable() {
public void run() {
handleRequest(conn);
}
});
} catch (RejectedExecutionException e) {
if (!exec.isShutdown())
log("task submission rejected", e);
}
}
}
public void stop() {
exec.shutdown();
}
private void log(String msg, Exception e) {
Logger.getAnonymousLogger().log(Level.WARNING, msg, e);
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if (isShutdownRequest(req))
stop();
else
dispatchRequest(req);
}
interface Request {
}
private Request readRequest(Socket s) {
return null;
}
private void dispatchRequest(Request r) {
}
private boolean isShutdownRequest(Request r) {
return false;
}
}
2.3 延遲任務和周期性任務
Java中提供Timer來執行延時任務和周期任務,但是Timer類有以下的缺陷:
- Timer只會創建一個線程來執行任務,如果有一個TimerTask執行時間太長,就會影響到其他TimerTask的定時精度;
- Timer不會捕捉TimerTask未定義的異常,所以當有異常拋出到Timer中時,Timer就會崩潰,而且也無法恢復,就會影響到已經被調度但是沒有執行的任務,造成“線程泄露”。
建議使用ScheduledThreadPoolExecutor來代替Timer類。
3. Callable & Future
如上文所說,Executor以Runnable的形式描述任務,但是Runnable有很大的局限性:
- 沒有返回值,只是執行任務;
- 不能處理被拋出的異常;
為了彌補以上的問題,Java中設計了另一種接口Callable。
public interface Callable<V> {
V call() throws Exception;
}
Callable支持任務有返回值,并支持異常的拋出。如果希望獲得子線程的執行結果,那Callable將比Runnable更為合適。
無論是Callable還是Runnable都是對于任務的抽象描述,即表明任務的范圍:有明確的起點,并且都會在一定條件下終止。
Executor框架下所執行的任務都有四種生命周期:
- 創建;
- 提交;
- 開始;
- 完成;
對于一個已提交但還沒有開始的任務,是可以隨時被停止;但是如果一個任務已經如果已經開始執行,就必須等到其相應中斷時再取消;當然,對于一個已經執行完成的任務,對其取消任務是沒有任何作用的。
既然任務有生命周期,那要如何才能知道一個任務當前的生命周期狀態呢?Callable既然有返回值,如何去在主線程中獲取子線程的返回值呢?為了解決這些問題,就需要Future類的幫助。
public interface Future<V> {
//取消任務
boolean cancel(boolean mayInterruptIfRunning);
// 任務是否被取消
boolean isCancelled();
// 任務是否完成
boolean isDone();
// 獲得任務的返回值
V get() throws InterruptedException, ExecutionException;
// 在超時期限內等待返回值
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future類表示任務生命周期狀態,其命名體現了任務的生命周期只能向前不能后退。
Future類提供方法查詢任務狀態外,還提供get方法獲得任務的返回值,但是get方法的行為取決于任務狀態:
- 如果任務已經完成,get方法則會立刻返回;
- 如果任務還在執行中,get方法則會擁塞直到任務完成;
- 如果任務在執行的過程中拋出異常,get方法會將該異常封裝為ExecutionException中,并可以通過getCase方法獲得具體異常原因;
如果將一個Callable對象提交給ExecutorService,submit方法就會返回一個Future對象,通過這個Future對象就可以在主線程中獲得該任務的狀態,并獲得返回值。
除此之外,可以顯式地把Runnable和Callable對象封裝成FutureTask對象,FutureTask不光繼承了Future接口,也繼承Runnable接口,所以可以直接調用run方法執行。
既然是并發處理,當然會遇到一次性提交一組任務的情況,這個時候可以使用CompletionService,CompletionService可以理解為Executor和BlockingQueue的組合:當一組任務被提交后,CompletionService將按照任務完成的順序將任務的Future對象放入隊列中。
CompletionService的接口如下:
public interface CompletionService<V> {
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
//如果隊列為空,就會阻塞以等待有任務被添加
Future<V> take() throws InterruptedException;
//如果隊列為空,就會返回null;
Future<V> poll();
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}
請注意take方法和poll方法的區別。
除了使用CompletionService來一個一個獲取完成任務的Future對象外,還可以調用ExecutorSerive的invokeAll()方法。
invokeAll支持限時提交一組任務(任務的集合),并獲得一個Future數組。invokeAll方法將按照任務集合迭代器的順序將任務對應的Future對象放入數組中,這樣就可以把傳入的任務(Callable)和結果(Future)聯系起來。當全部任務執行完畢,或者超時,再或者被中斷時,invokeAll將返回Future數組。
當invokeAll方法返回時,每個任務要么正常完成,要么被取消,即都是終止的狀態了。