要把程序变为并发程序,首先要理清各个任务之间的边界。在大多数服务器应用程序中都存在一个明显的任务边界:即单个客户请求。但有时候任务边界也并不是是显而易见的,好比在单个客户请求中仍有可能存在可发掘的并行性,例如数据库服务器。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做为单个计算的句柄是很是相似的。