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 作到这一点。图片

相关文章
相关标签/搜索