Java 并發之 Future 接口

jopen 11年前發布 | 85K 次閱讀 Java Java開發

簡介

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 做到這一點。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!