一步步優化頁面渲染功能
本節將模擬一個簡單的頁面渲染功能,它的作用是將HTML頁面繪制到圖像緩存中,為了簡便,假設HTML文件只包含標簽文本以及預訂大小的圖片和URL。
1、串行的頁面渲染器
最簡單的實現方式是對HTML文檔進行串行處理:先繪制文本,然后繪制圖像,串行處理:
public class SingleThreadRenderer {
void renderPage(CharSequence source) {
renderText(source);
List<ImageData> imageData = new ArrayList<ImageData>();
for (ImageInfo imageInfo : scanForImageInfo(source))
imageData.add(imageInfo.downloadImage());
for (ImageData data : imageData)
renderImage(data);
}
}
這種實現方式有個問題,因為圖像下載過程的大部分時間都是在等待I/O操作執行完成,在這期間CPU幾乎不做任何工作。因此,這種執行方式沒有充分地利用CPU,使得用戶在看到最終頁面之前要等待過長時間。通過將問題分解為多個獨立的任務並發執行,能夠活得更高的CPU利用率和響應靈敏度。
2、使用Future實現頁面渲染器
為了使頁面渲染器實現更高的並發性,首先將渲染過程分解為兩個任務,一個是渲染所有的文本,另一個是下載所有的圖像(一個是CPU密集型,一個是I/O密集型)。Callable和Future有助於表示這種協同任務的交互,以下代碼首先創建一個Callable來下載所有的圖像,當主任務需要圖像時,它會等待Future.get的調用結果。如果幸運的話,圖像可能已經下載完成,即使沒有,至少也已經提前開始下載。
public class FutureRenderer {
private final ExecutorService executor = Executors.newCachedThreadPool();
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) {
Thread.currentThread().interrupt();
future.cancel(true);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}
當然,我們還可以優化,用戶其實不需要等待所有圖像下載完成,我們可以每下載完一張圖像就立刻顯示出來。
3、使用CompletionService實現頁面渲染器
要實現下載完一張就立刻繪制,我們需要及時知道圖片下載完成,對於這種場景,CompletionService十分符合需求。CompletionService將生產新的異步任務與使用已完成任務的結果分離開來的服務。生產者 submit 執行的任務,使用者 take 已完成的任務,並按照完成這些任務的順序處理它們的結果。下面的代碼使用CompletionService改寫了頁面渲染器的實現:
public abstract 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++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}
為任務設置時限
有時候,如果某個任務無法在指定時間內完成,那么將不再需要它的結果,此時可以放棄這個任務。例如,某個Web應用程序從外部的廣告服務器上獲取廣告信息,但是如果該應用程序在兩秒內得不到響應,那么將顯示一個默認的廣告頁,這樣即使不能活得廣告信息,也不會降低站點的響應性能,對於這種需求,Future.get方法可以實現:
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = exec.submit(new FetchAdTask());
// Render the page while waiting for the ad
Page page = renderPageBody();
Ad ad;
try {
// Only wait for the remaining time budget
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
ad = DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad);
return page;
}
這種"預訂時間"的方法可以很容易地擴展到任意數量的任務上,考慮這樣一個旅行網站:用戶輸入旅行日期及要求,網站通過多種途徑獲取結果,此時,不應該讓頁面的響應時間受限於最慢的途徑,而應該只顯示在指定時間內收到的消息,我們可以通過使用支持限時的invokeAll,將多個任務提交到一個ExecutorService的方式實現這個需求:
public class TimeBudget {
private static ExecutorService exec = Executors.newCachedThreadPool();
public List<TravelQuote> getRankedTravelQuotes(TravelInfo travelInfo, Set<TravelCompany> companies,
Comparator<TravelQuote> ranking, long time, TimeUnit unit)
throws InterruptedException {
List<QuoteTask> tasks = new ArrayList<QuoteTask>();
for (TravelCompany company : companies)
tasks.add(new QuoteTask(company, travelInfo));
List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);
List<TravelQuote> quotes =
new ArrayList<TravelQuote>(tasks.size());
Iterator<QuoteTask> taskIter = tasks.iterator();
for (Future<TravelQuote> f : futures) {
QuoteTask task = taskIter.next();
try {
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(task.getFailureQuote(e.getCause()));
} catch (CancellationException e) {
quotes.add(task.getTimeoutQuote(e));
}
}
Collections.sort(quotes, ranking);
return quotes;
}
}
class QuoteTask implements Callable<TravelQuote> {
private final TravelCompany company;
private final TravelInfo travelInfo;
public QuoteTask(TravelCompany company, TravelInfo travelInfo) {
this.company = company;
this.travelInfo = travelInfo;
}
TravelQuote getFailureQuote(Throwable t) {
return null;
}
TravelQuote getTimeoutQuote(CancellationException e) {
return null;
}
public TravelQuote call() throws Exception {
return company.solicitQuote(travelInfo);
}
}
interface TravelCompany {
TravelQuote solicitQuote(TravelInfo travelInfo) throws Exception;
}
interface TravelQuote {
}
interface TravelInfo {
}
例子來自:《Java並發編程實戰》
