[本文是我对Java Concurrency In Practice 6.3的归纳和总结. 转载请注明作者和出处, 如有谬误, 欢迎在评论中指正. ]
浏览器的页面渲染模块负责HTML标记的处理, 本文以页面渲染为例探讨线程与并发. 为了简化问题, 我们假设只包含文本标记和图片标记.
单线程渲染
使用单线程处理是最简单的方式: 从头至尾扫描HTML文件, 如果遇到文本标记, 将其写入缓冲. 如果遇到图片标记, 就从Internet上下载后将其写入缓冲. 处理完整个文件之后, 将结果呈现给用户. 如果图片的下载速度很慢, 可能需要让用户等待很长时间. 因此我们对上述的渲染器进行简单的优化: 遇到图片标记, 就记录其下载地址, 并使用矩形的占位符. 处理完文件后先将结果呈现给用户, 然后再从Internet上下载图片, 图片下载完成就填充到占位符中:
public class SingleThreadRenderer {
void renderPage(CharSequence source) {
// 处理文本标记, 如果遇到图片标记使用占位符替代
renderText(source);
List<ImageData> imageData = new ArrayList<ImageData>();
// 从Internet上下载图片
for (ImageInfo imageInfo : scanForImageInfo(source))
imageData.add(imageInfo.downloadImage());
// 将下载完成的图片填充到占位符中
for (ImageData data : imageData)
renderImage(data);
}
}
在下载图片的过程中, 需要等待网络I/O, 在此期间, CPU没有得到充分的利用.
分步渲染
为了充分利用CPU资源, 并减少用户的等待时间, 我们将渲染拆分为2个任务: 一个任务负责渲染文本(主要占用CPU资源), 一个任务负责下载图片(主要占用网络I/O资源):
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 {
// get方法将阻塞, 直到task完成下载
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());
}
}
}
FutureRenderer先扫描文件, 找出所有的图片标记. 然后启动下载线程的同时进行文本渲染. 当文本渲染完成后调用future对象的get方法获取图片下载线程的下载结果. 如果调用get方法时下载任务尚未完成, get方法将阻塞, 直到下载完成, 或者抛出异常.
不对称任务的分析
FutureRenderer将渲染拆分为2个任务, 但是很有可能发生的是, 文本渲染任务很快就完成了, 但是下载所有图片的任务需要更长的时间. 这样的2个任务可以看做是不对称任务. 并发的引入导致问题比单线程时复杂很多, 而且并发并非是没有代价的, 因此拆分任务的时候一定要考虑拆分所带来的性能改善是否能够弥补其导致的损失.
CompletionService介绍
CompletionService组合了Executor和BlockingQueue的功能, ExecutorCompletionService实现了CompletionService接口. 当向ExecutorCompletionService对象提交任务时, 将任务包装成QueueingFuture对象, 然后再委托给ExecutorCompletionService内部的Executor执行:
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
// 将任务包装成QueueingFuture对象后委托给executor执行
executor.execute(new QueueingFuture(f));
return f;
}
QueueingFuture是FutureTask的子类, 其覆盖了FutureTask的done方法, done方法由系统在任务执行完成之后回调:
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
// 任务完成时将Future添加到已完成队列中
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
调用ExecutorCompletionService的take和poll方法可以从已完成队列中取出Future对象:
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
public Future<V> poll() {
return completionQueue.poll();
}
使用ExecutorCompletionService改进页面渲染
FutureRenderer将渲染拆分为2个不对称的任务, 此次我们将渲染拆分成多个任务: 一个图片下载一个任务:
public class Renderer {
private final ExecutorService executor;
Renderer(ExecutorService executor) { this.executor = executor; }
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService =
new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info)
// 将图片下载拆分为多个任务
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
// take方法可能阻塞: 当已完成队列中为空时
Future<ImageData> f = completionService.take();
// get方法不会阻塞, 因为从take方法返回的Future对象肯定是已完成的
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}
Renderer将图片下载拆分成多个任务, 解决了任务的不对称问题. 当图片下载完成后, 会以完成的顺序将Future添加到CompletionService对象的已完成队列中. 调用CompletionService对象的take方法可以从已完成队列中取出Future, 如果队列为空, take方法将阻塞, 直到队列不为空. 因此Renderer对图片的渲染按照下载完成的顺序进行(并非按照提交下载任务的顺序进行). Renderer具有不错的并发性能, 并且改善了渲染的响应速度.