java併發編程實踐學習(6 )任務執行

一.在線程中執行任務

圍繞執行任務來管理應用程序時,首先要清晰任務邊界。理想情況下,任務是獨立的活動:它的工作並不依賴於其他任務的狀態,結果,或者邊界效應。獨立有利於併發性,如果能得到相應的處理器資源,獨立的任務可以並行執行。
在正常的負載下,服務器應用程序應該兼具良好的吞吐量和快速的響應性。應用程序應該在負荷過載時平緩的劣化,而不是負載一高就簡單的任務失敗。所以我們要一個清晰的任務邊界,並配合一個明確的任務執行策略。
服務器的一般任務邊界:單獨的客戶請求,Web服務器,郵件服務器,文件服務器,EJB容器和數據庫服務器。將獨立的請求作爲任務邊界可以讓任務兼顧獨立性和適當的大小。

1.順序的執行任務

應用程序內部的任務調度可能有多種策略,這些策略可以在不同程度上發揮出潛在的併發性。其中最簡單的是單一的線程中順序的執行。
但是順序化處理幾乎不能爲服務器應用程序提供良好的吞吐量或快速的響應性。不過也有少數特例–比如任務的數量少但是生命週期長,或者服務器只能服務於唯一用戶,服務器只能在同一個時間能處理同一個請求。

2.顯式的爲任務創建線程

爲了更好的響應性,可以爲每個服務請求創建一個新線程。
WebServer爲每一個請求啓動一個新的線程

class ThreadTaskWebServer{
    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();
        }
    }
}

這樣

  • 執行任務的線程脫離主線程,主線程可以迅速等待下一個請求,從而提高響應性。
  • 並行處理任務。多個請求可以同時得到服務。
  • 任務代碼必須是線程安全的,

3.無限制創建線程的缺點

每任務每線程方法存在缺陷,尤其需要創建大量線程

  • 線程生命週期的開銷。創建和銷燬線程需要消耗大量的計算資源。
  • 資源消耗量。如果可運行的線程數多於可用處理器數,線程將空閒,會佔用大量內存,給垃圾回收帶來壓力。競爭CPU時還會帶來其他開銷,如果有足夠多的線程保持所有CPU忙碌,更多的線程會有百害而無一利。
  • 穩定性。 應該限制可創建線程的數量,根據不同的平臺和JVM的啓動參數,Thread的構造函數中請求棧的大小和底層操作系統的限制。如果超出限制會受到OutOfMemoryError限制。
    在一定範圍內,增加線程可以提高系統的吞吐量,一旦超出範圍更多的線程會讓程序崩潰。

二.Executor框架

線程池爲線程管理提供了同樣的好處。作爲Executor框架的一部分,java.util.concurrent提供了一個靈活的線程池實現。
Executor是一個簡單接口,它可以用於異步執行任務,有多種不同的執行策略,他還爲任務提交和任務執行之間的解耦提供了標準的方法,爲使用Runnable描述任務提供了通用的方式,還提供對生命週期的支持以及鉤子函數,可以添加統計收集,應用程序管理和監視器等拓展。
Executor基於商場消費者模式。提交任務的執行者是消費者,執行任務的線程時消費者,

1.使用Executor實現的Web Server

使用線程池的WebServer

class TaskExecutionWebServer{
    private static final int NTHREADS = 100;
    private static Executor exec = Executor.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) throws IOException{
        ServerSocket socket = new ServerSocket(80);
        while(true){
            final Socekt connection = socket.accept();
            Runnable task = new Runnable(){
                public void run(){
                    handleRquest(connection);
                }
            };
            exec.execute(task);
        }
    }
}

2.執行策略

將任務的提交與任務的執行進行解耦,可以簡單得爲一個類的任務制定執行策略,並保證後續的修改不至於太困難。
一個執行策略指明瞭任務執行的“ what、where、when、how”:

  • 任務在什麼(what)線程中執行 。
  • 任務以什麼(what)順序執行(FIFO、LIFO、優先級)?
  • 可以有多少(how many)個任務和併發執行?
  • 可以有多少個(how many)任務進入等待執行隊列?
  • 如果系統過載,需要放棄一個任務,應該挑選哪一個(which)?另外怎麼(how)通知應用程序知道。
  • 在一個任務執行前和後,應該做什麼(what)處理?
    執行策略是資源的管理工具。最佳的執行策略取決於可用的計算機資源和你對服務質量的需求。

3.線程池

線程池管理是一個工作者線程的同構池。線程池是與工作隊列精密綁定的。
工作隊列就是持有等待執行的任務。
線程池中執行多任務線程有很多優勢。重用線程而不是創建新的線程可以抵消創建銷往線程的開銷。在請求到達時工作線程就已經存在,提高了響應性。調整線程池的大小可以保持處理器忙碌。還可以防止過多的線程互相競爭資源。
你可以調用Executors中的靜態工廠方法來創建線程。
newFixedThreadPool創建一個定長的線程池。提交任務達到最大長度後不再變化。
newCachedThreadPool創建一個可以緩存的線程池,如果線程池的長度超過了處理的需要,他可以靈活的回收空閒的線程。當需求增加時,它可以靈活的添加新的線程,並且不會對池的長度做任何限制。
newSingleThreadExecutor創建一個單線程化的executor,它只創建唯一的工作者來執行任務,如果這個線程異常結束,會有另一個取代他。executor會保證依照任務隊列所規定的順序(FIFO、LIFO、優先級)執行。
newScheduledThreadPool創建一個定長的線程池,而且支持定時的以及週期性的執行任務類似於Timer。
newFixedThreadPool和newSingleThreadExecutor可以創建通用的ThreadPoolExecutor,直接使用ThreadPoolExecutor可以創建滿足自己需求的Executor。

4.Executor的生命週期

JVM在全部線程終止後纔會退出。如果無法正確關閉Executor會阻止JVM的結束。
ExecutorService中的生命走起方法

public interface ExecutorService extends Executor{
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout,TimeUnit unit)throws InterruptedException{
        //..其他用於任務提交的便利方法
    }
}

ExecutorService暗示了生命週期有三種狀態:運行(running)、關閉(shutting down)、和終止(terminated)。最初被創建的時候是運行狀態,shutdown方法會啓動一個平穩的關閉過程:停止接受新的任務,同時等待已經提交的任務完成(包括尚未開始的任務)。shoutdownNow方法會啓動一個強制關閉的過程:嘗試取消所有運行中的任務和排在隊列中尚未開始的任務。
關閉後提交到ExecutorService 中的任務會被拒絕執行處理器處理。拒絕執行處理器可能只是簡單的放棄任務,可能也會引起execute拋出一個未檢查的RejectedExecutionException、一旦所有任務完成後Executor會轉入終止狀態。可以調用awaitTerminal等待到達終止。也可以輪訓檢查isTerminated判斷是否終止。通常shutdown緊隨在awaitTermination之後可以同步關閉ExecutorService。

5.延遲的、並具週期性的任務

Timer工具管理任務的延遲執行以及週期性執行。但是timer存在一些缺陷,因此你可以考慮使用ScheduledThreadPoolExecutor作爲代替品。你可以通過構造函數或者通過newScheduledThreadPool工廠方法,創建一個ScheduledThreadPoolExecutor。
Timer只創建唯一的一個線程來執行所有的timer任務。如果一個timer任務的執行很耗時,會導致其他TimerTask的時效準確性出現問題。調度線程池解決了這個缺陷,他讓你可以提供多個線程來執行延遲、並具週期性的任務。Timer的另一個問題在於,如果TimerTask拋出未檢查異常,會終止timer線程。這種情況下,Timer也不會再重新恢復現成的執行。它認爲整個Timer都被取消了。(這個問題叫做線程泄露)
在java5.0以後幾乎沒有理由使用Timer
如果你需要構建自己的調度服務,仍然可以使用類庫提供的DelayQueue,它是BlockingQueue實現,爲ScheduledThreadPoolExecutor提供了調度功能。DlayQueue管理着一個包含Delayed對象的容器。每個Delayed對象都與一個延遲時間相關聯:只有元素過期後,DelayQueue才能讓你執行take操作獲取元素。從DelayQueue中返回的對象將根據它們所延遲的時間進行排序。

三.尋找可強化的並行性

Executor框架讓制定一個執行策略變得簡單。但你必須將任務描述爲Runnable。但是大多數應用服務器還存在這樣的請求:單一的客戶請求。即使這樣仍然會有進一步細化到並行性。

1.示例順執行的頁面渲染

處理HTML文檔最簡單的方法就是順序處理。當遇到一個文本標籤,就將它渲染到圖像緩存裏;當遇到一個圖像引用時,先通過網絡獲取它,然後也將它渲染到圖像緩存裏。這很容易實現但是用戶可能會等很長的時間。
另一種同樣是順序執行的方法可能會好一些,它先渲染文字,爲圖像留下矩形佔位符,處理完文本後再處理圖形。
但是下載圖像總得等待I/O完成,這時cpu幾乎是空閒的。通過將問題分散到可以併發執行的任務中可以獲得更好的cpu利用率和響應性。

public class SingleThreadRenderer {
    void renderPage(CharSequence source) {
        renderText(source);
        List<ImageData> imageData = new ArrayList<ImageData>();
        for (ImageInfo imageInfo : scanForImageInfo(source))
            imageData.add(imageInfo.downloadImage());
        for (ImageData data : imageData)
            renderImage(data);
    }
}

2.可攜帶結果的任務:Callable和Future

Executor使用Runnable作爲任務的基本表達形式,但是它不能返回一個值或者拋出受檢查的異常。
很多任務都有嚴重的計算延遲-數據庫查詢,網絡上獲取資源,進行復雜計算。這時Callable是更好的:他在主進入點-call-等待返回值,併爲可能拋出的異常預先做好了準備。Executors包含一些工具可以將其他任務類型封裝成一個Callable。
Callable和Runnable描述的是抽象的計算任務。一個Executor執行任務的生命週期有:創建、提交、開始、完成。由於執行時間可能可能很長,我們希望可以取消任務。在Executor框架中,總可以取消已經提交但是未開始的任務。但是已經開始的任務只有響應中斷才能取消。取消對已經完成的任務沒有影響。
任務的狀態決定了get方法的行爲:

  • 如果任務已經完成,get會立即返回或者拋出一個exception。
  • 如果任務沒有完成,get會阻塞直到它完成。
  • 如果任務拋出了異常,get會將該異常封裝爲ExecutionException然後拋出,可以用getCause獲取被封裝的原始異常。
  • 如果任務被取消,get會拋出CancellationException。

ExecutorService中所有submit方法都返回一個Future。因此你可以將一個Runnable或者Callable提交給executor,然後得到一個Furure。用它來重新獲取任務的執行結果或者取消任務。也可以顯示的給Runnable或者Callable實例化一個FutureTask(FutureTask實現了Runnable,所以既可以將它提交給Executor來執行,也可以直接調用run方法運行)

3. 示例:使用Future實現頁面渲染器

爲了使頁面渲染器實現更高的併發性,首先將渲染過程分解爲兩個任務,一個是渲染所有的文本,另一個是下載所有的圖像(一個是CPU密集型,一個是I/O密集型)。Callable和Future有助於表示這種協同任務的交互,以下代碼首先創建一個Callable來下載所有的圖像,當主任務需要圖像時,它會等待Future.get的調用結果。如果幸運的話,圖像可能已經下載完成,即使沒有,至少也已經提前開始下載。

public class FutureRenderer {
    private final ExecutorService executor = Executors.newCachedThreadPool();

    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) {
            Thread.currentThread().interrupt();
            future.cancel(true);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

當然,我們還可以優化,用戶其實不需要等待所有圖像下載完成,我們可以每下載完一張圖像就立刻顯示出來。

4.並行運行異類任務的侷限性

當我們試圖將不同的任務分配給不同的線程執行時,各個任務的大小可能完全不同,可能其中一個線程很快的完成而另一個很慢。這樣對性能的提升並不是很高,卻使代碼的複雜度大大提高。
大量同類的任務進行併發處理,會將程序的任務量分配到不同的任務中,這樣才能真正的獲得性能的提升。

5.CompletionService:當Executor遇見BlockingQueue

CompletionService整合了Executor和BlockingQueue的功能,你可以將Callable任務提交給它執行,然後使用類似於隊列中的take和poll方法,在結果完整可用時獲得這個結果,像一個打包的Future.
ExecutorCompletionService是實現CompletionService接口的一個類,並將計算任務委託給一個Executor。它在構造函數中創建一個BlockingQueue,用它去保存完成的結果。計算完成時會調用Futuretask中的done方法。當提交一個任務後,首先吧任務包裝爲一個QueueingFuture,它是FutureTask的一個子類,然後覆寫done方法將結果置入BlockingQueue中。

6.示例:使用CompletionService的頁面渲染器

要實現下載完一張就立刻繪製,我們需要及時知道圖片下載完成,對於這種場景,CompletionService十分符合需求。CompletionService將生產新的異步任務與使用已完成任務的結果分離開來的服務。生產者 submit 執行的任務,使用者 take 已完成的任務,並按照完成這些任務的順序處理它們的結果。
下面的代碼使用CompletionService改寫了頁面渲染器的實現

public abstract class Renderer {
    private final ExecutorService executor;

    Renderer(ExecutorService executor) {
        this.executor = executor;
    }

    void renderPage(CharSequence source) {
        final List<ImageInfo> info = scanForImageInfo(source);
        CompletionService<ImageData> completionService =
                new ExecutorCompletionService<ImageData>(executor);
        for (final ImageInfo imageInfo : info)
            completionService.submit(new Callable<ImageData>() {
                public ImageData call() {
                    return imageInfo.downloadImage();
                }
            });

        renderText(source);

        try {
            for (int t = 0, n = info.size(); t < n; t++) {
                Future<ImageData> f = completionService.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

7.爲任務限時

有時候如果一個活動無法在某個確定的時間內完成,那麼他的結果就是失敗了,此時應該放棄該活動。Future.get的限時版本符合這個條件;它在結果準備好後立刻返回,如果在時限內沒有準備好就會拋出TimeoutException。如果超時,你可以立刻停止任務,或者通過取消任務,這樣不會爲無用的任務繼續浪費資源。爲了完成這個需求,你可以在有限時的get拋出TimeoutException時通過Future取消任務。
在預定時間內獲取廣信息

Page renderPageWithAd() throws InterruptedException {
    long endNanos = System.nanoTime() + TIME_BUDGET;
    Future<Ad> f = exec.submit(new FetchAdTask());
    // Render the page while waiting for the ad
    Page page = renderPageBody();
    Ad ad;
    try {
        // Only wait for the remaining time budget
        long timeLeft = endNanos - System.nanoTime();
        ad = f.get(timeLeft, NANOSECONDS);
    } catch (ExecutionException e) {
        ad = DEFAULT_AD;
    } catch (TimeoutException e) {
        ad = DEFAULT_AD;
        f.cancel(true);
    }
    page.setAd(ad);
    return page;
}

8.示例:旅遊預訂門戶網站

在預定時間內請求旅遊報價

public class QuoteTask implements Callable<TravelQuote>{
    private final TravelCompany company;
    private final TravelInfo travelInfo;
    ....
    public TravelQuote call() throws Exception {
        return company.solicitQuote(travelInfo);
    }
}


 public List<TravelQuote> getRankedTravelQuotes(
 TravelInfo travelInfo, Set<TravelCompany> companies,Comparator<TravelQuote> ranking, long time, TimeUnit unit
 ) throws InterruptedException {
        List<QuoteTask> tasks = new ArrayList<QuoteTask>();
        for (TravelCompany company : companies)
            tasks.add(new QuoteTask(company, travelInfo));

        List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);

        List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size());
        Iterator<QuoteTask> taskIter = tasks.iterator();
        for (Future<TravelQuote> f : futures) {
            QuoteTask task = taskIter.next();
            try {
                quotes.add(f.get());
            } catch (ExecutionException e) {
                quotes.add(task.getFailureQuote(e.getCause()));
            } catch (CancellationException e) {
                quotes.add(task.getTimeoutQuote(e));
            }
        }

        Collections.sort(quotes, ranking);
        return quotes;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章