Java 併發之 Future 接口

簡介

Future 是 Java 5 JUC 包中的一個接口,主要提供了三類功能:java

任務結果的獲取

這個功能由 get 方法提供,它有兩種形式的重載。get 方法自己使用起來很簡單,須要注意的是它所拋出的異常:多線程

  • ExecutionException 對 Callable 或 Runnable 所拋出的異常的封裝,能夠經過 Throwable.getCause() 方法得到具體異常。
  • CancellationException 在調用 get 時任務被經過 Future.cancel() 方法被取消所拋出的異常。這個是 運行時異常,但若是你有調用 Future.cancel() 的地方,那仍是須要處理的。
  • TimeoutException V get(long timeout, TimeUnit unit) 重載形式所拋出的超時異常。

任務取消

經過代碼看 Future 的使用

咱們先看一段代碼,這個代碼是《Java Concurrency in Practise》的 「Listing 6.13. Waiting for Image Download with Future.」。併發

<!-- lang: java -->
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());
		}
	}
}

這段代碼模擬了一個 HTML 網頁渲染的過程。整個渲染過程分紅 HTML 文本的渲染和圖片的下載及渲染。這段代碼爲了提升渲染效率,先提交圖片的下載任務,而後在渲染文本,文本渲染完畢以後再去渲染圖片。因爲圖片下載是 IO 密集操做,HTML 文本渲染是 CPU 密集操做,因此讓二者併發運行能夠提升效率。異步

Future 的侷限性

獲取已完成的任務

看到這裏,確定會有人說,爲何只用一個線程去下載全部的圖片。若是用多線程去下載圖片,效率豈不是更高。的確是這樣,可是在提交圖片下載以後,如何去從多個 Future 那裏得到下載結果呢?依次調用 Future.get() 是個解決辦法,可是那樣效率並不高,由於第一個有多是下載速度最慢的,這樣會拖累整個頁面的渲染,由於咱們但願下載完一個圖片就渲染一個。ide

爲了解決這個問題,咱們能夠這樣寫線程

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.");
}

這段代碼是否是很長,其實咱們不用這麼辛苦,JDK 已經替咱們考慮了這個問題。可是這個話題超出了本期範圍,我會在接下來的文章裏講到如何更好地解決這個問題。code

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 作到這一點。圖片

相關文章
相關標籤/搜索