Java併發程式設計實戰系列6之任務執行(Task Execution)

choubou發表於2021-09-09

1. 線上程中執行任務

1.1 序列的執行任務

這是最經典的一個最簡單的Socket server的例子,伺服器的資源利用率非常低,因為單執行緒在等待I/O操作完成時,CPU處於空閒狀態。從而阻塞了當前請求的延遲,還徹底阻止了其他等待中的請求被處理。

public class SingleThreadWebServer {    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);        while (true) {
            Socket connection = socket.accept();
            handleRequest(connection);
        }
    }    private static void handleRequest(Socket connection) {        // request-handling logic here
    }
}

1.2 顯式地為任務建立執行緒

任務處理從主執行緒中分離出來,主迴圈可以快速等待下一個連線,提高響應性。同時任務可以並行處理了,吞吐量也提高了。

public class ThreadPerTaskWebServer {    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();
        }
    }    private static void handleRequest(Socket connection) {        // request-handling logic here
    }
}

1.3 無限制建立執行緒的不足

  • 執行緒的生命週期開銷非常高

  • 資源消耗。大量的空閒執行緒佔用記憶體,給GC帶來壓力,同時執行緒數量過多,競爭CPU資源開銷太大。
    穩定性。容易引起GC問題,甚至OOM

2  Executor框架

任務就是一組邏輯工作單元(unit of work),而執行緒則是使任務非同步執行的機制。

Executor介面,是代替Thread來做非同步執行的入口,介面雖然簡單,卻為非常靈活強大的非同步任務執行框架提供了基礎。
提供了一種標準的方法將任務的提交與執行過程解耦,並用Runnable(無返回時)或者Callable(有返回值)表示任務。

Executor基於生產者-消費者模式
提交任務/執行任務分別相當於生產者/消費者,通常是最簡單的實現生產者-消費者設計的方式了

2.1 基於Executor改造後的樣例如下

圖片描述


將請求處理任務的提交與任務的實際執行解耦,並且只需採用另一種不同的Executor實現,就可以改變伺服器的行為,其影響遠遠小於修改任務提交方式帶來的影響

2.2 執行策略

這一節主要介紹做一個Executor框架需要靠那些點?

在什麼執行緒中執行任務?
任務按照什麼順序執行?FIFO/LIFO/優先順序
有多少個任務可以併發執行?
佇列中允許多少個任務等待?
如果系統過載了要拒絕一個任務,那麼選擇拒絕哪一個?如何通知客戶端任務被拒絕了?
在執行任務過程中能不能有些別的動作before/after或者回撥?
各種執行策略都是一種資源管理工具,最佳的策略取決於可用的計算資源以及對服務質量的要求。

因此每當看到
new Thread(runnable).start();
並且希望有一種靈活的執行策略的時候,請考慮使用Executor來代替

2.3 執行緒池

線上程池中執行任務比為每個任務分配一個執行緒優勢明顯:

重用執行緒,減少開銷。
延遲低,執行緒是等待任務到達。
最大化挖掘系統資源以及保證穩定性。CPU忙碌但是又不會出現執行緒競爭資源而耗盡記憶體或者失敗的情況。
Executors可以看做一個工廠,提供如下幾種Executor的建立:

newCachedThreadPool
newFixedThreadPool
newSingleThreadExecutor
newScheduledThreadPool

2.4 Executor的生命週期

為解決執行服務的生命週期問題,Executor擴充套件了ExecutorService介面,新增了一些用於生命週期管理的方法


圖片描述

一個優雅停止的例子:


圖片描述


增加生命週期擴充套件Web伺服器的功能

  • 呼叫stop

  • 客戶端請求形式

關閉

2.5 延遲任務與週期任務

使用Timer的弊端在於

  • 如果某個任務執行時間過長,那麼將破壞其他TimerTask的定時精確性(執行所有定時任務時只會建立一個執行緒),只支援基於絕對時間的排程機制,所以對系統時鐘變化敏感

  • TimerTask丟擲未檢查異常後就會終止定時執行緒(不會捕獲異常)

更加合理的做法是使用ScheduledThreadPoolExecutor,只支援基於相對時間的排程
它是DelayQueue的應用場景

3 找出可利用的並行性

3.1 攜帶結果的任務Callable和Future

Executor框架支援Runnable,同時也支援Callable(它將返回一個值或者丟擲一個異常)
在Executor框架中,已提交但是尚未開始的任務可以取消,但是對於那些已經開始執行的任務,只有他們能響應中斷時,才能取消。
Future非常實用,他的API如下


圖片描述


內部get的阻塞是靠LockSupport.park來做的,在任務完成後Executor回撥finishCompletion方法會依次喚醒被阻塞的執行緒。

ExecutorService的submit方法接受Runnable和Callable,返回一個Future。ThreadPoolExecutor框架留了一個口子,子類可以重寫newTaskFor來決定建立什麼Future的實現,預設是FutureTask類。

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

圖片描述

3.3 CompletionService: Executor與BlockingQueue

計算完成後FutureTask會呼叫done方法,而CompletionService整合了FutureTask,對於計算完畢的結果直接放在自己維護的BlockingQueue裡面,這樣上層呼叫者就可以一個個take或者poll出來。


圖片描述

3.3 示例:使用CompletionService提高渲染效能

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

Future的get支援timeout。

使用invokeAll方法提交List<Callable>,返回一個List<Future>



作者:芥末無疆sss
連結:
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4548/viewspace-2816167/,如需轉載,請註明出處,否則將追究法律責任。

相關文章