導語:很多開發者談到Java多執行緒開發,僅僅停留在new Thread(…)t()或直接使用Executor框架這個層面,對於執行緒的管理和控制卻不夠深入,下面是 Java多執行緒的開發技巧,一起來學習下吧:
不使用執行緒池的缺點
有些開發者圖省事,遇到需要多執行緒處理的地方,直接new Thread(…)t(),對於一般場景是沒問題的,但如果是在併發請求很高的情況下,就會有些隱患:
1. 新建執行緒的開銷。執行緒雖然比程序要輕量許多,但對於JVM來說,新建一個執行緒的代價還是挺大的,決不同於新建一個物件。
2. 資源消耗量。沒有一個池來限制執行緒的數量,會導致執行緒的數量直接取決於應用的併發量,這樣有潛在的執行緒資料巨大的可能,那麼資源消耗量將是巨大的。
3. 穩定性。當執行緒數量超過系統資源所能承受的程度,穩定性就會成問題。
制定執行策略
在每個需要多執行緒處理的地方,不管併發量有多大,需要考慮執行緒的執行策略:
1. 任務以什麼順序執行
2. 可以有多少個任務併發執行
3. 可以有多少個任務進入等待執行佇列
4. 系統過載的時候,應該放棄哪些任務?如何通知到應用程式?
5. 一個任務的執行前後應該做什麼處理
執行緒池的型別
不管是通過Executors建立執行緒池,還是通過Spring來管理,都得清楚知道有哪幾種執行緒池:
FixedThreadPool:定長執行緒池,提交任務時建立執行緒,直到池的最大容量,如果有執行緒非預期結束,會補充新執行緒
CachedThreadPool:可變執行緒池,它猶如一個彈簧,如果沒有任務需求時,它回收空閒執行緒,如果需求增加,則按需增加執行緒,不對池的大小做限制
SingleThreadExecutor:單執行緒。處理不過來的任務會進入FIFO佇列等待執行
SecheduledThreadPool:週期性執行緒池。支援執行週期性執行緒任務
其實,這些不同型別的執行緒池都是通過構建一個ThreadPoolExecutor來完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory這麼幾個引數。具體可以參見JDK DOC。
執行緒池飽和策略
由以上執行緒池型別可知,除了CachedThreadPool其他執行緒池都有飽和的可能,當飽和以後就需要相應的策略處理請求執行緒的任務,ThreadPoolExecutor採取的方式通過佇列來儲存這些任務,當然會根據池型別不同選擇不同的佇列,比如FixedThreadPool和SingleThreadExecutor預設採用的是無限長度的LinkedBlockingQueue。但從系統可控性講,最好的做法是使用定長的ArrayBlockingQueue或有限的LinkedBlockingQueue,並且當達到上限時通過ejectedExecutionHandler方法設定一個拒絕任務的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy幾種策略,具體差異可見JDK DOC
執行緒無依賴性
多執行緒任務設計上儘量使得各任務是獨立無依賴的,所謂依賴性可兩個方面:
執行緒之間的依賴性。如果執行緒有依賴可能會造成死鎖或飢餓
呼叫者與執行緒的依賴性。呼叫者得監視執行緒的完成情況,影響可併發量
當然,在有些業務裡確實需要一定的依賴性,比如呼叫者需要得到執行緒完成後結果,傳統的'Thread是不便完成的,因為run方法無返回值,只能通過一些共享的變數來傳遞結果,但在Executor框架裡可以通過Future和Callable實現需要有返回值的任務,當然執行緒的非同步性導致需要有相應機制來保證呼叫者能等待任務完成,關於Future和Callable的用法見下面的例項就一目瞭然了:
01 public class FutureRenderer {
02 private final ExecutorService executor = ...;
03 void renderPage(CharSequence source) {
04 final List
05 Callable<list
06 new Callable<list
07 public List
08 List
09 = new ArrayList
10 for (ImageInfo imageInfo : imageInfos)
11 (loadImage());
12 return result;
13 }
14 };
15 Future<list
16 renderText(source);
17 try {
18 List
19 for (ImageData data : imageData)
20 renderImage(data);
21 } catch (InterruptedException e) {
22 // Re-assert the thread's interrupted status
23 entThread()rrupt();
24 // We don't need the result, so cancel the task too
25 el(true);
26 } catch (ExecutionException e) {
27 throw launderThrowable(ause());
28 }
29 }
30 }
以上程式碼關鍵在於List imageData = ();如果Callable型別的任務沒有執行完時,呼叫者會阻塞等待。不過這樣的方式還是得謹慎使用,很容易造成不良設計。另外對於這種需要等待的場景,就需要設定一個最大容忍時間timeout,設定方法可以在 ()加上timeout引數,或是再呼叫keAll 加上timeout引數
執行緒的取消與關閉
一般的情況下是讓執行緒執行完成後自行關閉,但有些時候也會中途取消或關閉執行緒,比如以下情況:
呼叫者強制取消。比如一個長時間執行的任務,使用者點選”cancel”按鈕強行取消
限時任務
發生不可處理的任務
整個應用程式或服務的關閉
因此需要有相應的取消或關閉的方法和策略來控制執行緒,一般有以下方法:
1)通過變數標識來控制
這種方式比較老土,但使用得非常廣泛,主要缺點是對有阻塞的操作控制不好,程式碼示例如下所示:
01 public class PrimeGenerator implements Runnable {
02 @GuardedBy("this")
03 private final List
04 = new ArrayList
05 private volatile boolean cancelled;
06 public void run() {
07 BigInteger p = ;
08 while (!cancelled ) {
09 p = ProbablePrime();
10 synchronized (this) {
11 (p);
12 }
13 }
14 }
15 public void cancel() { cancelled = true; }
16 public synchronized List
17 return new ArrayList
18 }
19 }
2)中斷
中斷通常是實現取消最明智的選擇,但執行緒自身需要支援中斷處理,並且要處理好中斷策略,一般響應中斷的方式有兩種:
處理完中斷清理後繼續傳遞中斷異常(InterruptedException)
呼叫interrupt方法,使得上層能感知到中斷異常
3) 取消不可中斷阻塞
存在一些不可中斷的阻塞,比如:
和中同步讀寫IO
Selector的非同步IO
獲取鎖
對於這些執行緒的取消,則需要特定情況特定對待,比如對於socket阻塞,如果要安全取消,則需要呼叫e()
4)JVM的關閉
如果有任務需要在JVM關閉之前做一些清理工作,而不是被JVM強硬關閉掉,可以使用JVM的鉤子技術,其實JVM鉤子也只是個很普通的技術,也就是用個map把一些需要JVM關閉前啟動的任務儲存下來,在JVM關閉過程中的某個環節來併發啟動這些任務執行緒。具體使用示例如下:
1 public void start() {
2 untime()hutdownHook(new Thread() {
3 public void run() {
4 try { (); }
5 catch (InterruptedException ignored) {}
6 }
7 });
8 }