《Java併發編程實戰》學習筆記(4)

第六章:任務執行

大多數併發應用程序是圍繞執行任務(task)進行管理的。所謂任務就是抽象、離散的工作單元(unit of work)。

  • 把一個應用程序的工作(work)分離到任務中,可以簡化程序的管理;
  • 這種分離還在不同事務間劃分了自然的分界線,可以方便程序在出現錯誤時進行恢復;
  • 同時這種分離還可以爲並行工作提供一個自然的結構,有利於提高程序的併發性。

在線程中執行任務

理想情況下,任務是獨立的活動:它的工作並不依賴於其他任務的狀態、結果或者邊界效應(side effect)。

獨立有利於併發性,如果能得到相應的處理器資源,獨立的任務還可以並行執行。

在正常的負載下,服務器應用程序應該兼具良好的吞吐量快速的響應性

進一步講,應用程序應該在負荷過載時平緩地劣化,而不應該負載一高就簡單地以失敗告終。

爲了達到這些目的,你要選擇一個清晰的任務邊界,並配合一個明確的任務執行策略

大多數服務器應用程序都選擇了下面這個自然的任務邊界:單獨的客戶請求。

Web服務器,郵件服務器,文件服務器,EJB 容器和數據庫服務器,這些服務器都接受遠程客戶通過網絡連接發送的請求。

將獨立的請求作爲任務邊界,可以讓任務兼顧獨立性和適當的大小。

例如,向郵件服務器提交一個消息後產生的結果,並不會被其他正在同時處理的消息所影響。


順序地執行任務

應用程序內部的任務調度,存在多種可能的調度策略,這些策略可以在不同程度上發揮出潛在的併發性。

其中最簡單的策略是在單一的線程中順序地執行任務。

/**
 * 順序化的 Web Server
 */
class SingleThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            Socket connection = socket.accept();
            handleRequest(connection);
        }
    }
}

就這個示例而言,主線程不斷地在“接受連接”與“處理相關請求”之間交替運行,並且直到主線程完成了當前的請求並再次調用accept,此前新的請求都必須等待。

一個Web請求的處理包括執行運算與進行IO操作。

服務器必須處理Socket I/O,以讀取請求和寫回響應,網絡擁堵或連通性問題會導致這個操作阻塞。

服務器還要處理文件I/O、發送數據庫請求,這些同樣會引起操作的阻塞。

這在生產環境中的執行效率會很糟糕。

在某些情況下,順序化處理在簡單性或者安全性上具有優勢;大多數GUI框架使用單一的線程,並順序地處理任務。
我們會在第9章再次討論順序化模型。


顯示地爲任務創建線程

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

/**
 * Web Server爲每個請求啓動一個新的線程
 */
class ThreadPerTaskWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            Runnable task = () -> handleRequest(connection);
            new Thread(task).start();
        }
    }
}

ThreadPerTaskWebServer在結構上類似於單線程版本——主線程仍然不斷地交替運行“接受外部連接”與“轉發請求”。

不同點在於,主循環爲每個連接都創建一個新線程以處理請求,而不是在主循環的內部處理它。由此得出下面3條主要結論:

  • 執行任務的負載已經脫離了主線程,這讓主循環能夠更迅速地重新開始等待下一個連接。這使得程序可以在完成前面的請求之前接受新的請求,從而提高了響應性。
  • 並行處理任務, 這使得多個請求可以同時得到服務。如果有多個處理器,或者出於I/O未完成、鎖請求以及資源可用性等任何因素需要阻塞任務時,程序的吞吐量會得到提高。
  • 任務處理代碼必須是線程安全的,因爲有多個任務會併發地調用它。

在中等強度的負載水平下,“每任務每線程( thread-per-task)”方法是對順序化執行的良好改進。

只要請求的到達速度尚未超出服務器的請求處理能力,那麼這種方法可以同時帶來更快的響應性和更大的吞吐量。.


無限制創建線程的缺點

當用於生產環境中時,“ 每任務每線程(thread-per-task) ”方法存在一些實際的缺陷,尤其在需要創建大量的線程時會更加突出:

  • 線程生命週期的開銷。線程的創建與關閉不是“免費”的。實際的開銷依據不同平臺而不同,但是創建線程的確需要時間,帶來處理請求的延遲,並且需要在JVM和操作系統之間進行相應的處理活動。如果請求是頻繁的且輕量的,就像大多數服務器程序一樣,那麼爲每個請求創建一個新線程的做法就會消耗大量的計算資源。
  • 資源消耗。活動線程會消耗系統資源,尤其是內存。如果可運行的線程數多於可用的處理器數,線程將會空閒。大量空閒線程佔用更多內存,給垃圾回收器帶來壓力,而且大量線程在競爭CPU資源時,還會產生其他的性能開銷。如果你已經有了足夠多的線程保持所有CPU忙碌,那麼再創建更多的線程是有百害而無一利的。
  • 穩定性。應該限制可創建線程的數目。限制的數目依不同平臺而定,同時也受到JVM的啓動參數、Thread 的構造函數中請求的棧大小等因素的影響,以及底層操作系統線程的限制。如果你打破了這些限制,最可能的結果是收到一個OutOfMemoryError。企圖從這種錯誤中恢復是非常危險的,更簡單的辦法是構造你的程序時避免超出這些限制。

Executor框架

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