Java併發實戰(六) 任務執行

吳小爽發表於2018-03-09

1.任務

把程式抽象成多個任務。

2.現代web程式劃分任務邊界

以獨立的客戶請求為邊界。就是一個請求一個任務。

3.任務排程策略

3.1序列的執行

糟糕的響應性和吞吐量。

3.2為每一個任務建立一個執行緒

結論:

  • 任務交由子執行緒處理,提高了響應性和吞吐量。
  • 任務處理的程式碼必須是執行緒安全的。

不足:

  • 執行緒生命週期的開銷非常高。
  • 資源消耗。可執行執行緒多於可用處理器的數量,會有執行緒閒置佔用記憶體,且大量執行緒競爭CPU時將產生其他效能開銷。
  • 穩定性。不同平臺可建立執行緒的數量有限制。

4.Java中Executor框架的設計

4.1設計理念

Java提供了Executor框架來執行任務。基於生產者-消費者模式。提交任務就是操作相當於生產者,執行任務的執行緒相當於消費者。(解耦,削峰)

    public interface Executor {
        void execute(Runnable command);
    }
複製程式碼

4.2執行策略

任務的提交程式碼散佈在整個程式的業務程式碼中。
執行策略則統一交由框架處理。

執行策略中定義了任務執行的"What,Where,When,How"等方面,包括:

  • 在什麼(What)執行緒中執行任務?
  • 任務按照什麼(What)順序執行(優先順序)?
  • 有多少個(How Many)任務能併發執行?
  • 在佇列中有多少個(How Many)任務在等待執行?
  • 系統該怎麼(How)拒絕任務?
  • 在任務執行前後,應該進行哪些(What)動作?

通過將任務提交與任務的執行策略分離,有助於在部署階段選擇與可用硬體資源最匹配的執行策略。

4.3執行緒池

Executor任務執行框架將"為每一個任務分配一個執行緒"策略程式設計基於執行緒池的策略。
類庫提供了一個靈活的執行緒池及一些有用的預設配置。如newFixedThreadpool。

  • Web伺服器不會再高負載情況下失敗。
  • 但是任務到達的速度總是超過任務執行的速度,伺服器仍有可能耗盡記憶體。

4.4Executor的生命週期

Executor擴充套件了ExecutorService介面,新增了一些用於生命週期管理的方法。

    public interface ExecutorService extends Executor {
        /**
         * 平緩的關閉過程:不再接受新任務,等待已經提交的任務執行完成。
         */
        void shutdown();
        
        /**
         * 粗暴的關閉過程:它將嘗試取消所有執行中的任務,不在啟動佇列中尚未開始執行的任務。
         */
        list<Runnable> shutdownNow();
        
        boolean isShutdown();
        boolean isTerminated();
        boolean awaitTermination(long timeout, TimeUnit unit);
    }
複製程式碼

4.5延遲任務和週期任務

JAVA中提供Timer來管理定時任務。

  • Timer執行定時任務只會建立一個執行緒。
  • Timer是基於絕對時間的排程機制,對系統時間敏感。
  • Timer存線上程洩露問題(Timer不捕獲異常,當丟擲一個未檢查異常時執行緒將終止)。

ScheduledThreadPoolExecutor更優質的管理定時任務。

  • 其內部是一個執行緒池。
  • 其很好的解決了Timer的執行緒洩露問題。

不適用於分散式環境。

5.找出可利用的並行性

本章提供一些示例來發掘在一個請求中的並行性。

5.1 示例:序列的頁面渲染器

假設頁面 = 文字標籤 + 圖片
如下程式碼序列的執行渲染。

    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);
        }
    }
複製程式碼

5.2攜帶結果的任務Callable與Future

Runnable作為基本的任務表現形式。缺陷:1.無返回值。2.不能丟擲一個受檢查異常。

  • Callable介面

它是任務更好的抽象,描述了一個任務的返回值和異常。

    public interface Callable<V> {
        V call() throws Exception;
    }
複製程式碼
  • Future介面

它表示一個任務的生命週期,並提供了相應的方法來判斷任務是否已經完成或取消。

    public interface Future<V> {
        boolean cancel(boolean mayInterruptIfRunning);
        boolean isCancelled();
        boolean isDone();
        V get() throws Exception;
        V get(long timeout, TimeUnit unit);
    }
複製程式碼

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

將渲染過程分解成兩個任務,一個是渲染所有的文字,一個是下載所有影像。

    程式碼略。
複製程式碼

渲染文字和渲染圖片併發執行。

5.4在異構任務並行化中存在的侷限

上例中一般渲染文字的速度遠遠高於渲染圖片的速度,程式最終和序列執行效率差別不大,程式碼確變得更復雜了。

只有大量相互獨立且同構的任務可以併發進行處理時,才能體現出效能的提升。

5.5CompletionService:Executor與BlockingQueue

提交一組任務,簡單的寫法。

    @Test
    public void test() throws Exception{
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List<Future<String>> futures = new ArrayList();
        for (int i=0; i<5; i++){
            final int param = i;
            Future<String> future = executor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(param * 1000);
                    return "result" + param;
                }
            });
            futures.add(future);
        }

        for (int i=4; i>0; i--) {
            System.out.println(futures.get(i).get());
        }
    }
複製程式碼

CompletionService將Executor和BlockingQueue的功能融合。你可以將Callable任務提交給它執行,然後使用類似佇列操作的take和poll方法來獲得已完成的結果。

5.6示例:使用CompletionService實現頁面渲染器

書上的示例:略。

    @Test
    public void test() throws Exception{
        ExecutorService executor = Executors.newFixedThreadPool(5);
        CompletionService<String> completionService = new ExecutorCompletionService<>(executor);
        for (int i=4; i>0; i--){
            final int param = i;
            completionService.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(param * 1000);
                    return "result" + param;
                }
            });
        }
        for (int i=0; i<4; i++) {
            System.out.println(completionService.take().get());
        }
    }
    輸出:
        result1
        result2
        result3
        result4
複製程式碼

5.7為任務設定時限

為單個任務設定時間。

    @Test
    public void singleTaskTest(){
        ExecutorService executor = Executors.newFixedThreadPool(5);
        Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() {
                try {
                    Thread.sleep(2000L);
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println("任務執行完畢...");
                return "singleTask.";
            }
        });
        try {
            System.out.println(future.get(1, TimeUnit.SECONDS));
        }catch (TimeoutException e){
            System.out.println("任務超時...");
            future.cancel(true); // 這句話的是否登出影響執行情況,原理未知?
        }catch (InterruptedException e){
            e.printStackTrace();
        }catch (ExecutionException e){
            e.printStackTrace();
        }
    }
複製程式碼

5.8示例:陸行預定入口網站

未多個任務設定超時時間。

6.總結

本章主要是介紹了Java的Executor框架的優點和一些常見需求。
還有對任務的劃分粒度,要根據業務場景分析任務邊界。

相關文章