- 相關(guān)推薦
Java多線程的開發(fā)技巧
導(dǎo)語:很多開發(fā)者談到Java多線程開發(fā),僅僅停留在new Thread(…).start()或直接使用Executor框架這個層面,對于線程的管理和控制卻不夠深入,下面是 Java多線程的開發(fā)技巧,一起來學(xué)習(xí)下吧:
不使用線程池的缺點
有些開發(fā)者圖省事,遇到需要多線程處理的地方,直接new Thread(…).start(),對于一般場景是沒問題的,但如果是在并發(fā)請求很高的情況下,就會有些隱患:
1. 新建線程的開銷。線程雖然比進程要輕量許多,但對于JVM來說,新建一個線程的代價還是挺大的,決不同于新建一個對象。
2. 資源消耗量。沒有一個池來限制線程的數(shù)量,會導(dǎo)致線程的數(shù)量直接取決于應(yīng)用的并發(fā)量,這樣有潛在的線程數(shù)據(jù)巨大的可能,那么資源消耗量將是巨大的。
3. 穩(wěn)定性。當線程數(shù)量超過系統(tǒng)資源所能承受的程度,穩(wěn)定性就會成問題。
制定執(zhí)行策略
在每個需要多線程處理的地方,不管并發(fā)量有多大,需要考慮線程的執(zhí)行策略:
1. 任務(wù)以什么順序執(zhí)行
2. 可以有多少個任務(wù)并發(fā)執(zhí)行
3. 可以有多少個任務(wù)進入等待執(zhí)行隊列
4. 系統(tǒng)過載的時候,應(yīng)該放棄哪些任務(wù)?如何通知到應(yīng)用程序?
5. 一個任務(wù)的執(zhí)行前后應(yīng)該做什么處理
線程池的類型
不管是通過Executors創(chuàng)建線程池,還是通過Spring來管理,都得清楚知道有哪幾種線程池:
FixedThreadPool:定長線程池,提交任務(wù)時創(chuàng)建線程,直到池的最大容量,如果有線程非預(yù)期結(jié)束,會補充新線程
CachedThreadPool:可變線程池,它猶如一個彈簧,如果沒有任務(wù)需求時,它回收空閑線程,如果需求增加,則按需增加線程,不對池的大小做限制
SingleThreadExecutor:單線程。處理不過來的任務(wù)會進入FIFO隊列等待執(zhí)行
SecheduledThreadPool:周期性線程池。支持執(zhí)行周期性線程任務(wù)
其實,這些不同類型的線程池都是通過構(gòu)建一個ThreadPoolExecutor來完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory這么幾個參數(shù)。具體可以參見JDK DOC。
線程池飽和策略
由以上線程池類型可知,除了CachedThreadPool其他線程池都有飽和的可能,當飽和以后就需要相應(yīng)的策略處理請求線程的任務(wù),ThreadPoolExecutor采取的方式通過隊列來存儲這些任務(wù),當然會根據(jù)池類型不同選擇不同的隊列,比如FixedThreadPool和SingleThreadExecutor默認采用的是無限長度的LinkedBlockingQueue。但從系統(tǒng)可控性講,最好的做法是使用定長的ArrayBlockingQueue或有限的LinkedBlockingQueue,并且當達到上限時通過ThreadPoolExecutor.setRejectedExecutionHandler方法設(shè)置一個拒絕任務(wù)的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy幾種策略,具體差異可見JDK DOC
線程無依賴性
多線程任務(wù)設(shè)計上盡量使得各任務(wù)是獨立無依賴的,所謂依賴性可兩個方面:
線程之間的依賴性。如果線程有依賴可能會造成死鎖或饑餓
調(diào)用者與線程的依賴性。調(diào)用者得監(jiān)視線程的完成情況,影響可并發(fā)量
當然,在有些業(yè)務(wù)里確實需要一定的依賴性,比如調(diào)用者需要得到線程完成后結(jié)果,傳統(tǒng)的Thread是不便完成的,因為run方法無返回值,只能通過一些共享的變量來傳遞結(jié)果,但在Executor框架里可以通過Future和Callable實現(xiàn)需要有返回值的任務(wù),當然線程的異步性導(dǎo)致需要有相應(yīng)機制來保證調(diào)用者能等待任務(wù)完成,關(guān)于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 result.add(imageInfo.downloadImage());
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 Thread.currentThread().interrupt();
24 // We don't need the result, so cancel the task too
25 future.cancel(true);
26 } catch (ExecutionException e) {
27 throw launderThrowable(e.getCause());
28 }
29 }
30 }
以上代碼關(guān)鍵在于List imageData = future.get();如果Callable類型的任務(wù)沒有執(zhí)行完時,調(diào)用者會阻塞等待。不過這樣的方式還是得謹慎使用,很容易造成不良設(shè)計。另外對于這種需要等待的場景,就需要設(shè)置一個最大容忍時間timeout,設(shè)置方法可以在 future.get()加上timeout參數(shù),或是再調(diào)用ExecutorService.invokeAll 加上timeout參數(shù)
線程的取消與關(guān)閉
一般的情況下是讓線程運行完成后自行關(guān)閉,但有些時候也會中途取消或關(guān)閉線程,比如以下情況:
調(diào)用者強制取消。比如一個長時間運行的任務(wù),用戶點擊”cancel”按鈕強行取消
限時任務(wù)
發(fā)生不可處理的任務(wù)
整個應(yīng)用程序或服務(wù)的關(guān)閉
因此需要有相應(yīng)的取消或關(guān)閉的方法和策略來控制線程,一般有以下方法:
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 = BigInteger.ONE;
08 while (!cancelled ) {
09 p = p.nextProbablePrime();
10 synchronized (this) {
11 primes.add(p);
12 }
13 }
14 }
15 public void cancel() { cancelled = true; }
16 public synchronized List
17 return new ArrayList
18 }
19 }
2)中斷
中斷通常是實現(xiàn)取消最明智的選擇,但線程自身需要支持中斷處理,并且要處理好中斷策略,一般響應(yīng)中斷的方式有兩種:
處理完中斷清理后繼續(xù)傳遞中斷異常(InterruptedException)
調(diào)用interrupt方法,使得上層能感知到中斷異常
3) 取消不可中斷阻塞
存在一些不可中斷的阻塞,比如:
java.io和java.nio中同步讀寫IO
Selector的異步IO
獲取鎖
對于這些線程的取消,則需要特定情況特定對待,比如對于socket阻塞,如果要安全取消,則需要調(diào)用socket.close()
4)JVM的關(guān)閉
如果有任務(wù)需要在JVM關(guān)閉之前做一些清理工作,而不是被JVM強硬關(guān)閉掉,可以使用JVM的鉤子技術(shù),其實JVM鉤子也只是個很普通的技術(shù),也就是用個map把一些需要JVM關(guān)閉前啟動的任務(wù)保存下來,在JVM關(guān)閉過程中的某個環(huán)節(jié)來并發(fā)啟動這些任務(wù)線程。具體使用示例如下:
1 public void start() {
2 Runtime.getRuntime().addShutdownHook(new Thread() {
3 public void run() {
4 try { LogService.this.stop(); }
5 catch (InterruptedException ignored) {}
6 }
7 });
8 }
【Java多線程的開發(fā)技巧】相關(guān)文章:
java的多線程12-04
java多線程03-27
java語言的多線程11-25
關(guān)于Java多線程介紹04-02
Java多線程問題總結(jié)11-27
高級Java多線程面試題及回答11-30
java多線程同步塊實例講解素材04-02
舉例講解Java中的多線程范文欣賞04-03
JAVA多線程之線程間的通信方式解析04-02