java併發編程實踐學習(8) 應用線程池

一.任務執行策略間的隱性耦合

Executor框架可以將任務的提交和執行策略解耦,但是並非能夠適合所有的執行策略。有些任務需要明確指定一個執行策略:

  1. 依賴性任務。如果你提交給線程池的任務要依賴於其它的任務,你就隱式的給執行策略帶來了約束。這樣你就必須仔細的管理執行策略以避免活躍度的問題。
  2. 採用線程限制任務。單線程化的Executor相比於任意線程池,可以對同步作出更強的承諾。它可以保證任務不會併發的執行,允許你放寬任務代碼對於線程安全的要求。可以把對象限制在任務線程中,不需要同步。
  3. 對響應時間敏感的任務。將一個長時間運行的任務提交到單線程化的Executor中,或者將多個長時間的任務提交給一個只包含少量線程的線程池中,這樣會削弱由Executor管理的服務的響應性。
  4. **使用ThreadLocal任務。**ThreadLocal讓每個線程可以保留一份變量的私有版本。但是隻要條件允許,Executor就會隨意重用這些線程。標準的Executor的實現是 :在需求不高時回收空閒的線程,在需求增加時添加新的線程,如果任務拋出了異常就會被限制在當前的任務中,在線程池中使用ThreadLocal纔有意義,在線程池中不應該使用ThreadLocal傳遞人物間的數值。

當任務都是同類、獨立的時候,線程池纔會有最佳的工作表現。如果將耗時的與短期的任務混在一起,除非線程池夠大,否則會有“塞車”的危險。如果提交的任務要依賴於其他任務,除非線程池很大,否則有產生死鎖的危險。

一些任務有這樣的特徵:需要或排斥某種特定的執行策略。對其他任務具有依賴性的任務就會要求線程池足夠大,來保證他所以來的任務不必排隊或者不被拒絕;採用線程限制的任務需要順序的執行。把這些寫入文檔方便維護。

1.線程飢餓死鎖

如果線程池在一個任務中依賴其他任務的執行,就可能產生死鎖。在一個線程池中,如果所有線程執行的任務都阻塞在線程池中,等待仍然處於同一工作隊列的其他任務,就會發生死鎖,這樣的死鎖被叫做線程飢餓死鎖

2.耗時操作

如果任務由於過長時間週期而阻塞,即使不出現死鎖線程池的響應性也會變得很差。
限定任務等待資源的時間可以緩解耗時操作帶來的影響。如果等待超時,你可以把任務標示爲失敗,終止它或者重新放回隊列。這樣無論每個任務成功還是失敗都能使容我向前發展。

3.定製線程池的大小

線程池合力的大小取決於未來提交任務的類型和所部署的系統特徵。池的長度應該有某種配置機制提供,或者用Runtime.availableProcessors的結果動態的計算。
爲了正確的定製線程池的長度,你需要理解你的計算環境、資源預算和任務的自身特性。部署系統中安裝了多少個CPU?多少內存?任務主要執行的是計算、I/O還是一些混合操作?它們是否需要像JDBC Connection這樣的稀缺資源?如果你有不同類別的任務,它們擁有差別很大的行爲,那麼應該考慮使用多個不同的線程池,這樣每個線程池可以根據不同任務的工作負載進行調節。
對於計算密集型的任務,一個有N 個處理器的系統通常通過使用一個N +1個線程的線程池來獲得最優的利用率(計算密集型的線程恰好在某時因爲發生一個頁錯誤或者因爲其他原因而暫停,剛好有一個“額外”的線程,可以確保在這樣的情況下CPU週期不會中斷工作)。對於包含了I/O和其他阻塞操作的任務,不是所有的線程都會在所有的時間被調度,因此你需要一個更大的池。爲了正確地設置線程池的長度,你必須估算出任務花在等待的時間與用來計算的時間的比率;這個估算值不必十分精確,而且可以通過一些監控工具獲得。你還可以選擇另一種方法來調節線程池的大小,在一個基準負載下,使用不同大小的線程池運行你的應用程序,並觀察CPU利用率的水平。
給定下列定義:

     N = CPU的數量
     U = 目標CPU的使用率,0 ≤ Ucpu≤ 1
     W/C = 等待時間與計算時間的比率
     爲保持處理器達到期望的使用率,最優的池的大小等於:
     Nthreads = N * U * ( 1 + W/C )
     可以使用Runtime來獲得CPU的數目:
     int N_CPUS = Runtime.getRuntime().availableProcessors();

CPU週期並不是唯一可以使用線程池管理的資源。其他可以約束資源池大小的資源包括:內存、文件句柄、套接字句柄和數據庫連接等。計算這些資源池的大小約束非常簡單:首先累加出每一個任務需要的這些資源的總量,然後除以可用的總量。所得的結果是池大小的上限。
當任務需要使用池化的資源時,比如數據庫連接,那麼線程池的長度和資源池的長度會互相影響。如果每一個任務都需要一個數據庫連接,那麼線程池的大小就限制了線程池的有效大小;類似的,當線程池中的任務是線程池的唯一消費者時,那麼線程池的大小反而又會限制了連接池的有效大小。

三.配置 ThreadPoolExecutor

ThreadPoolExcutor爲一些Executor提供了基本的實現,這些Executor是由Executors中的工廠 newCahceThreadPool、newFixedThreadPool和newScheduledThreadExecutor返回的。 ThreadPoolExecutor是一個靈活的健壯的池實現,允許各種各樣的用戶定製。
如果默認的執行策略無法滿足你的需求你可以通過構造函數自己創建一個ThreadPoolExecutor
最常用的ThreadPoolExecutor構造請參見這裏寫鏈接內容

1.線程的創建與銷燬

核心池大小、最大池大小和存活時間共同管理着線程的創建與銷燬。核心池的大小是目標的大小;線程池的實現試圖維護池的大小;即使沒有任務執行,池的大小也等於核心池的大小,並直到工作隊列充滿前,池都不會創建更多的線程。如果當前池的大小超過了核心池的大小,線程池就會終止它。最大池的大小是可同時活動的線程數的上限。如果一個線程已經閒置的時間超過了存活時間,它將成爲一個被回收的候選者。
調整核心大小和存活時間可以促進歸還空閒線程佔有的資源,讓這些資源用於更有用的工作。

  • newFixedThreadPool工廠爲請求的池設置了核心池的大小和最大池的大小,而且池永遠不會超時
  • newCacheThreadPool工廠將最大池的大小設置爲Integer.MAX_VALUE,核心池的大小設置爲0,超時設置爲一分鐘。這樣創建了無限擴大的線程池,會在需求量減少的情況下減少線程數量
  • 其他的組合可以用顯示的ThreadPoolExecutor構造函數實現。

2.管理隊列任務

如果請求到來過快超過了服務器處理它們的速度就可能有資源耗盡的危險。

  • ThreadPoolExecutor允許你提供一個BlockingQueue來持有等待執行的任務。任務排隊有3種基本方法:無限隊列、有限隊列和同步移交。
  • newFixedThreadPool和newSingleThreadExectuor默認使用的是一個無限的 LinkedBlockingQueue。如果所有的工作者線程都處於忙碌狀態,任務會在隊列中等候。如果任務持續快速到達,超過了它們被執行的速度,隊 列也會無限制地增加。
  • 對於龐大或無限的池,可以使用SynchronousQueue,完全繞開隊列,直接將任務由生產者交給工作者線程。
  • 可以用LinkedBlockingQueue或ArrayBlockingQueue按到達數學處理任務,也可以使用PriorityBlockingQueue通過優先級安排任務。
  • newCahedThreadPool提供了比定長線程池更好的隊列等候性能,是很好的默認選擇。
    最穩妥的策略是使用有限隊列,比如ArrayBlockingQueue或有限的LinkedBlockingQueue以及 PriorityBlockingQueue。但是隊列滿後應該怎麼處理,這時就要依靠不同的飽和策略。
    一個大隊列加一個小池可以減少上下文切換,但會增加吞吐量的開銷

3.飽和策略

當有界隊列滿後,飽和策略開始起作用ThreadPoolExecutor的飽和策略可以通過setRejectedExecutionHandler修改。JDK提供了AbortPollicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
- 默認的AbortPollicy(中止)策略會引起拋出未檢查的 RejectedExecutionException;
- DiscardPolicy(遺棄)策略會默認放棄這個任務
- DiscardOldestPolicy(遺棄最舊的)策略會遺棄最舊的,如果是優先級隊列那麼會遺棄優先級最高的。
- CallerRunsPolicy(調用運行者)策略不會丟棄任務拋出異常,他會把任務推回調用者線程執行

4.線程工廠

線程池需要創建線程時要通過線程工廠完成。
ThreadFactory只有唯一的方法newThread。有很多原因需要使用定製的線程池:希望指明UncaughtExceptionHandler,實例化定製的Thread類,希望修改線程池的優先級或者後臺狀態、或者希望給線程一個名稱簡化轉儲和入住的解釋。
定製線程工廠

public class MyThreadFactory implements ThreadFactory{
    private final String poolName;

    public MyThreadFactory(String poolName){
        this.poolName = poolName;
    }

    public Thread newThread(Runnable runnable){
        return new MyAppThread(Runnable,poolName);
    }
}

5.構造後在定製ThreadPoolExecutor

ThreadPoolExecutor也可以咋在創建後通過setters修改,比如核心線程池的大小,存活時間,線程工廠和拒絕執行處理器。

四.擴展ThreadPoolExecutor

ThreadPoolExecutor是可擴展的它提供了幾個鉤子去“覆寫”:beforeExecute、afterExecute和terminate。
執行任務的線程會調用beforeExecute和afterExecute,無論是正常的執行完返回還是拋出異常afterExecute都會被調用。如果任務完成後拋出ErrorafterExecute不會被調用。如果before拋出一個RuntimeException任務將不執行afterExecute也不會被調用。terminated會在線程池完成關閉動作後調用。

//擴展線程池以提供日誌和計時功能  
public class TimingThreadPool extends ThreadPoolExecutor{  
    //需要重寫配置型的構造方法  
    public TimingThreadPool(int corePoolSize, int maximumPoolSize,  long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {  
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);  
    }  

    //執行任務之前  
    @Override  
    protected void beforeExecute(Thread t, Runnable r) {  
        super.beforeExecute(t, r);  
        System.out.println("執行任務之前~");  
    }  
    //執行任務之後  
    @Override  
    protected void afterExecute(Runnable r, Throwable t) {  
        super.afterExecute(r, t);  
        System.out.println("執行任務之後~");  
    }  
    //執行任務完成,需要執行關閉操作纔會調用這個方法  
    @Override  
    protected void terminated() {  
        super.terminated();  
        System.out.println("執行任務完成~");  
    } 
}  

5.並行遞歸算法

循環並行化可以應用與一些遞歸設計中。一種簡單的情況是,每個迭代不需要來自於它所調用的結果。
把順序遞歸轉化爲並行遞歸

public<T> void sequentialRecursive(List<Node<T> nodes, Collection<T> results){
    for(Node<T> n: nodes){
        results.add(n.compute());
        sequentialRecursive(n.getChildren(),results);
    }
}
public<T> void parallelrecursive(final Executor exec,List<Node<T> nodes, final Collection<T> results){
    for(final Node<T> n:nodes){
        exec.execute(new Runnable(){
            public void run(){
                results.add(n.compute());
            }
        });
        parallelRecursive(exec,n.getChildren(),results);
    }
}

等待並行的運算結果

public<T> Collection<T> getParallelResults(List<Node<T>> nodes)throws InterruptedExceptin{
    ExecutorService exec = Executors.newCachedThreadPool();
    Queue<T> resultQueue = new ConcurrentLinkedQueue<T>();
    parallelRecursive(exec,nodes,resultQueue);
    exec.shutdown();
    exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);
    return resultQueue;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章