Java多線程開發技巧

不少開發者談到Java多線程開發,僅僅停留在new Thread(...).start()或直接使用Executor框架這個層面,對於線程的管理和控制卻不夠深刻,經過讀《Java併發編程實踐》瞭解到了不少不爲我知但又很是重要的細節,今日整理以下。java

不該用線程池的缺點編程

有些開發者圖省事,遇到須要多線程處理的地方,直接new Thread(...).start(),對於通常場景是沒問題的,但若是是在併發請求很高的狀況下,就會有些隱患:安全

  • 新建線程的開銷。線程雖然比進程要輕量許多,但對於JVM來講,新建一個線程的代價仍是挺大的,決不一樣於新建一個對象多線程

  • 資源消耗量。沒有一個池來限制線程的數量,會致使線程的數量直接取決於應用的併發量,這樣有潛在的線程數據巨大的可能,那麼資源消耗量將是巨大的併發

  • 穩定性。當線程數量超過系統資源所能承受的程度,穩定性就會成問題框架

制定執行策略異步

在每一個須要多線程處理的地方,無論併發量有多大,須要考慮線程的執行策略socket

  • 任務以什麼順序執行this

  • 能夠有多少個任何併發執行spa

  • 能夠有多少個任務進入等待執行隊列

  • 系統過載的時候,應該放棄哪些任務?如何通知到應用程序?

  • 一個任務的執行先後應該作什麼處理

線程池的類型

不論是經過Executors建立線程池,仍是經過Spring來管理,都得清楚知道有哪幾種線程池:

  • FixedThreadPool:定長線程池,提交任務時建立線程,直到池的最大容量,若是有線程非預期結束,會補充新線程

  • CachedThreadPool:可變線程池,它猶如一個彈簧,若是沒有任務需求時,它回收空閒線程,若是需求增長,則按需增長線程,不對池的大小作限制

  • SingleThreadExecutor:單線程。處理不過來的任務會進入FIFO隊列等待執行

  • SecheduledThreadPool:週期性線程池。支持執行週期性線程任務

其實,這些不一樣類型的線程池都是經過構建一個ThreadPoolExecutor來完成的,所不一樣的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory這麼幾個參數。具體能夠參見JDK DOC。

線程池飽和策略

由以上線程池類型可知,除了CachedThreadPool其餘線程池都有飽和的可能,當飽和之後就須要相應的策略處理請求線程的任務,ThreadPoolExecutor採起的方式經過隊列來存儲這些任務,固然會根據池類型不一樣選擇不一樣的隊列,好比FixedThreadPool和SingleThreadExecutor默認採用的是無限長度的LinkedBlockingQueue。但從系統可控性講,最好的作法是使用定長的ArrayBlockingQueue或有限的LinkedBlockingQueue,而且當達到上限時經過ThreadPoolExecutor.setRejectedExecutionHandler方法設置一個拒絕任務的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy幾種策略,具體差別可見JDK DOC

線程無依賴性

多線程任務設計上儘可能使得各任務是獨立無依賴的,所謂依賴性可兩個方面:

  • 線程之間的依賴性。若是線程有依賴可能會形成死鎖或飢餓

  • 調用者與線程的依賴性。調用者得監視線程的完成狀況,影響可併發量

固然,在有些業務裏確實須要必定的依賴性,好比調用者須要獲得線程完成後結果,傳統的Thread是不便完成的,由於run方法無返回值,只能經過一些共享的變量來傳遞結果,但在Executor框架裏能夠經過Future和Callable實現須要有返回值的任務,固然線程的異步性致使須要有相應機制來保證調用者能等待任務完成,關於Future和Callable的用法見下面的實例就一目瞭然了:

public class FutureRenderer {
    private final ExecutorService executor = ...;
    void renderPage(CharSequence source) {
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        Callable<List<ImageData>> task =
                new Callable<List<ImageData>>() {
                    public List<ImageData> call() {
                        List<ImageData> result
                                = new ArrayList<ImageData>();
                        for (ImageInfo imageInfo : imageInfos)
                            result.add(imageInfo.downloadImage());
                        return result;
                    }
                };
        Future<List<ImageData>> future =  executor.submit(task);
        renderText(source);
        try {
            List<ImageData> imageData =  future.get();
            for (ImageData data : imageData)
                renderImage(data);
        } catch (InterruptedException e) {
            // Re-assert the thread's interrupted status
            Thread.currentThread().interrupt();
            // We don't need the result, so cancel the task too
            future.cancel(true);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

以上代碼關鍵在於List<ImageData> imageData = future.get();若是Callable類型的任務沒有執行完時,調用者會阻塞等待。不過這樣的方式仍是得謹慎使用,很容易形成不良設計。另外對於這種須要等待的場景,就須要設置一個最大容忍時間timeout,設置方法能夠在 future.get()加上timeout參數,或是再調用ExecutorService.invokeAll 加上timeout參數

線程的取消與關閉 

通常的狀況下是讓線程運行完成後自行關閉,但有些時候也會中途取消或關閉線程,好比如下狀況:

  • 調用者強制取消。好比一個長時間運行的任務,用戶點擊"cancel"按鈕強行取消

  • 限時任務

  • 發生不可處理的任務

  • 整個應用程序或服務的關閉

所以須要有相應的取消或關閉的方法和策略來控制線程,通常有如下方法:

1)經過變量標識來控制

這種方式比較老土,但使用得很是普遍,主要缺點是對有阻塞的操做控制很差,代碼示例以下所示:

public class PrimeGenerator implements Runnable {
     @GuardedBy("this")
     private final List<BigInteger> primes
             = new ArrayList<BigInteger>();
     private  volatile boolean cancelled;
     public void run() {
         BigInteger p = BigInteger.ONE;
         while (!cancelled ) {
             p = p.nextProbablePrime();
             synchronized (this) {
                 primes.add(p);
             }
         }
     }
     public void cancel() { cancelled = true;  }
     public synchronized List<BigInteger> get() {
         return new ArrayList<BigInteger>(primes);
     }
}

2)中斷

中斷一般是實現取消最明智的選擇,但線程自身須要支持中斷處理,而且要處理好中斷策略,通常響應中斷的方式有兩種:

  • 處理完中斷清理後繼續傳遞中斷異常(InterruptedException)

  • 調用interrupt方法,使得上層能感知到中斷異常

3) 取消不可中斷阻塞

存在一些不可中斷的阻塞,好比:

  • java.io和java.nio中同步讀寫IO

  • Selector的異步IO

  • 獲取鎖

對於這些線程的取消,則須要特定狀況特定對待,好比對於socket阻塞,若是要安全取消,則須要調用socket.close()

4)JVM的關閉

若是有任務須要在JVM關閉以前作一些清理工做,而不是被JVM強硬關閉掉,可使用JVM的鉤子技術,其實JVM鉤子也只是個很普通的技術,也就是用個map把一些須要JVM關閉前啓動的任務保存下來,在JVM關閉過程當中的某個環節來併發啓動這些任務線程。具體使用示例以下:

public void start() {
    Runtime.getRuntime().addShutdownHook(new Thread() {
        public void run() {
            try { LogService.this.stop(); }
            catch (InterruptedException ignored) {}
        }
    });
}
相關文章
相關標籤/搜索