Java 并發之 Future 接口
簡介
Future 是 Java 5 JUC 包中的一個接口,主要提供了三類功能:
任務結果的獲取
這個功能由 get 方法提供,它有兩種形式的重載。get 方法本身使用起來很簡單,需要注意的是它所拋出的異常:
- ExecutionException 對 Callable 或 Runnable 所拋出的異常的封裝,可以通過
Throwable.getCause()
方法獲得具體異常。 - CancellationException 在調用 get 時任務被通過
Future.cancel()
方法被取消所拋出的異常。這個是 運行時異常,但如果你有調用Future.cancel()
的地方,那還是需要處理的。 - TimeoutException
V get(long timeout, TimeUnit unit)
重載形式所拋出的超時異常。
</ul>
任務取消
通過代碼看 Future 的使用
我們先看一段代碼,這個代碼是《Java Concurrency in Practise》的 “Listing 6.13. Waiting for Image Download with Future.”。
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 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) { // Re-assert the thread's interrupted status Thread.currentThread().interrupt(); // We don't need the result, so cancel the task too future.cancel(true); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } }
}</pre>
這段代碼模擬了一個 HTML 網頁渲染的過程。整個渲染過程分成 HTML 文本的渲染和圖片的下載及渲染。這段代碼為了提高渲染效率,先提交圖片的下載任務,然后在渲染文本,文本渲染完畢之后再去渲染圖片。由于圖片下載是 IO 密集操作,HTML 文本渲染是 CPU 密集操作,所以讓兩者并發運行可以提高效率。
Future 的局限性
獲取已完成的任務
看到這里,肯定會有人說,為什么只用一個線程去下載所有的圖片。如果用多線程去下載圖片,效率豈不是更高。的確是這樣,但是在提交圖片下載之后,如何去從多個 Future 那里獲得下載結果呢?依次調用 Future.get() 是個解決辦法,但是那樣效率并不高,因為第一個有可能是下載速度最慢的,這樣會拖累整個頁面的渲染,因為我們希望下載完一個圖片就渲染一個。
為了解決這個問題,我們可以這樣寫
public void renderPage(CharSequence source) { List<ImageInfo> imageInfos = scanForImageInfo(source);Queue<Future<ImageData>> imageDownloadFutures = new LinkedList<Future<ImageData>>(); for (final ImageInfo imageInfo : imageInfos) { Future<ImageData> future = executorService.submit(new Callable<ImageData>() { @Override public ImageData call() throws Exception { return imageInfo.downloadImage(); } }); imageDownloadFutures.add(future); } renderText(source); Future<ImageData> future; while ((future = imageDownloadFutures.poll()) != null) { if (future.isDone()) { if (!future.isCancelled()) { try { renderImage(future.get()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // We don't need the result, so cancel the task too future.cancel(true); } catch (ExecutionException e) { System.out.println(e.getMessage()); renderImage(ImageData.emptyImage()); } } } else { imageDownloadFutures.add(future); } try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println("Interrupt images download."); } } executorService.shutdownNow(); System.out.println("Finish the page render.");
}</pre>
這段代碼是不是很長,其實我們不用這么辛苦,JDK 已經替我們考慮了這個問題。但是這個話題超出了本期范圍,我會在接下來的文章里講到如何更好地解決這個問題。
Future 的使用范圍
從上面的例子我們看到,Future 是有其局限性的。Future 主要功能在于獲取任務執行結果和對異步任務的控制。但如果要獲取批量任務的執行結果,從上面的例子我們已經可以看到,單使用 Future 是很不方便的。其原因在于:一是我們沒有好的方法去獲取第一個完成的任務;二是 Future.get 是阻塞方法,使用不當會造成線程的浪費。解決第一個問題可以用 CompletionService 解決,CompletionService 提供了一個 take() 阻塞方法,用以依次獲取所有已完成的任務。對于第二個問題,可以用 Google Guava 庫所提供的 ListeningExecutorService 和 ListenableFuture 來解決。這些都會在后面的介紹。
除了獲取批量任務執行結果時不便,Future 另外一個不能做的事便是防止任務的重復提交。要做到這件事就需要 Future 最常見的一個實現類 FutureTask 了。《Java Concurrency in Practice》中的例子“Listing 5.19. Final Implementation of Memoizer”便展示了如何使用 FutureTask 做到這一點。