要把程序變爲併發程序,首先要理清各個任務之間的邊界。在大多數服務器應用程序中都存在一個明顯的任務邊界:即單個客戶請求。但有時候任務邊界也並不是是顯而易見的,好比在單個客戶請求中仍有可能存在可發掘的並行性,例如數據庫服務器。java
本文將開發一些不一樣版本的組件,而且每一個版本都實現了不一樣程度的併發性。該示例組件實現瀏覽器程序中的頁面渲染(Page-Rendering)功能,它的做用是將HTML頁面繪製到圖像緩存中。爲了簡便,假設HTML頁面只包含標籤文本,以及預約義大小的圖片和URL。數據庫
最簡單的方法就是對HTML文檔進行串行處理。當遇到文本標籤時,將其繪製到圖像緩存中。當遇到圖像引用時,先經過網絡獲取它,而後再將其繪製到圖像緩存中。這種方式很容易實現,程序只需將輸入中的每一個元素處理一次(甚至不須要緩存文檔),但這種方法會讓用戶等待很長時間,直到顯示全部的文本。瀏覽器
另外一種串行執行的方法會好一點,它先處理文本標籤,同時爲圖像預留出矩形的佔位空間,在處理完了第一遍文本後,程序在開始下載圖像,並將它們繪製到相應的佔位空間中:緩存
public class SingleThreadRenderer { void renderPage(CharSequence source) { //先處理文本 renderText(source); //掃描整個文檔,找出全部的圖片,而後下載 List<ImageInfo> imageInfos = scanForImageInfo(source); List<ImageData> imageDatas = new ArrayList<ImageData>(); for (ImageInfo info : imageInfos) { imageDatas.add(info.downloadImage()); } //渲染全部下載的圖片 for (ImageData data : imageDatas) { renderImage(data); } //整個頁面渲染完成 } }
圖像下載過程的大部分時間都是在等待I/O操做執行完成,這期間CPU幾乎不做任何工做。所以,這種串行執行方法沒有充分的利用CPU,使得用戶在看到最終頁面以前要等待過長的時間。經過將問題分解爲多個獨立的任務併發執行,可以得到更高的CPU利用率和響應速度。服務器
爲了實現更高的併發性,首先將渲染過程分解爲兩個任務,一個是渲染全部的文本標籤,另外一個是下載全部的圖像(由於其中一個任務時CPU密集型,而另外一個任務時I/O密集型,所以這種方法即便在單CPU系統上也能提高性能)。網絡
Callable和Future有助於表示這些協同任務之間的交互。下面的FutureRenderer中建立了一個Callable來下載全部的圖像,並將其提交到一個ExecutorService。這將返回一個描述任務執行狀況的Future。當主任務須要圖像時,它會等待Future.get的調用結果。若是幸運的話,當前開始請求時,全部的圖像就已經下載完成了,即便沒有,至少圖像的下載任務已經提早開始了:併發
public class FutureRenderer { private final ExecutorService executor = ...; 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 info : imageInfos) { ImageData data = info.downloadImage(); result.add(data); } return result; } }; //將任務提交到Executor中執行 Future<List<ImageData>> future = executor.submit(task); //處理文本 renderText(source); //獲取全部已下載好的圖片 try { List<ImageData> imageDatas = future.get();//將阻塞直處處理完成或執行出錯 //渲染全部下載的圖片 for (ImageData data : imageDatas) { renderImage(data); } } catch(InterruptedException e) { Thread.currentThread().interrupt(); future.cancel(); } catch(ExecutionException e) { throw launderThrowable(e.getCause()); } //整個頁面渲染完成 } }
這裏先介紹一下Future,Runnable和Callable函數
Runnable和Callable描述的都是抽象的計算任務。這些任務一般是有範圍的,即都有一個明確的起點,而且最終都會結束。性能
Future表示一個任務的生命週期,並提供了相應的方法來判斷是否已經完成或取消。Future還提供了get方法來得到計算結果。get方法的行爲取決於任務的執行狀態(還沒有開始,正在執行,已完成)。this
FutureRenderer使得渲染文本任務與下載圖像的任務併發的執行。當全部圖像下載完後,會顯示到頁面上。這將提高用戶 體驗,不只使用戶更快的看到結果,還有效利用了並行性。但咱們還能夠作得更好——用戶沒必要等到全部的圖像都下載完成,而是但願看到每當下載完一幅圖像時就當即顯示出來。
CompletionService將Executor和BlockingQueue的功能融合到了一塊兒。你能夠將Callable任務提交給它來執行,而後使用相似於隊列操做的take和poll等方法來得到已完成的結果,而這些結果會在完成時被封裝爲Future。
咱們能夠經過CompletionService從兩個方面來提升頁面渲染的性能:縮短總運行時間以及提升響應性。爲每一幅圖像的下載都建立一個獨立任務,並在線程池中執行它們,從而將下載過程轉換爲並行過程,這將減小下載全部圖像的總時間。此外,經過從CompletionService中獲取結果以及使每張圖片在下載完成好偶馬上顯示出來,能使用戶得到一個更加動態和更高響應性的用戶界面:
public class Renderer { private final ExecutorService executor; //構造函數 public Renderer(ExecutorService executor) { this.executor = executor; } void renderPage(CharSequence source) { //掃描整個文檔,找出全部的圖片 final List<ImageInfo> imageInfos = scanForImageInfo(source); //使用CompletionService併發處理下載圖片的任務 CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor); for (final ImageInfo info : imageInfos) { completionService.submit(new Callable<ImageData>(){ public ImageData call() { return info.downloadImage(); } }); } //處理文本 renderText(source); //獲取全部已下載好的圖片 try { //渲染全部下載的圖片 for (int i = 0; i < imageInfos.size(); i++) { Future<ImageData> future = completionService.take(); ImageData img = future.get();//將阻塞直處處理完成或執行出錯 renderImage(img); } } catch(InterruptedException e) { Thread.currentThread().interrupt(); } catch(ExecutionException e) { throw launderThrowable(e.getCause()); } //整個頁面渲染完成 } }
多個ExecutorCompletionService能夠共享一個Executor,所以能夠建立一個對於特定計算私有,又能共享一個公共Executor的ExecutorCompletionService。所以CompletionService的做用就至關於一組計算的句柄,這與Future做爲單個計算的句柄是很是相似的。