在做应用开发时,有时会遇到一种场景:在完成某个主任务的同时,需要处理一些其它的子任务,为了加快响应速度,会将子任务放在另外的线程中执行。例如,搜索引擎在处理用户的查询请求时,不仅要从本地数据库查找匹配的结果,同时可能会向远程的广告服务器请求广告数据,最后将搜索结果和广告一起返回给用户。在这个例子中,主线程从本地查找搜索结果,同时启动子线程从远程服务器获取广告数据,当主线程查找结束时,会将子线程得到的广告数据与搜索结果合并,最后发送给用户。Java代码如下: 01 | public void searchService(query) { |
03 | AdThread adThread = new AdThread(); |
主线程启动子线程后,子线程的执行已不受主线程的控制,其何时执行完毕,主线程无法预知,而其执行结果是由主线程来主动索取的。为了做到这一点,就需要用到Future模式。 Future模式是现实中提货单的抽象,好比去摄影店拍照,照片需要过些时候才能洗出来,而我们不可能一直等下去,商家一般会给我们一张单据,并告知第二天10:00以后凭此单领取照片,而我们就可以暂时离开去做其它事情,等到第二天再带着单据来到摄影店领取照片,如果我们9:30就到了,照片还没有洗出来,我们就会继续等一会儿,直到照片洗出来。以下代码用Future模式实现前述的主线程和广告子线程之间的协作: 主线程: 01 | public void searchService(query) { |
03 | FutureAd future = startAdThread(); |
05 | SearchResult result = findResult(query); |
07 | Ad ad = future.getAd(); |
09 | respond2User(result, ad); |
13 | FutureAd startAdThread() { |
15 | FutureAd future = new FutureAd(); |
17 | new AdThread(future).start(); |
广告线程: 01 | public class AdThread extends Thread { |
03 | private FutureAd future; |
05 | public AdThread(FutureAd ad) { |
14 | Ad ad = retrieveAdFromRemoteServer(); |
18 | } catch (Exception e) {} |
关键部分是FutureAd对象: 01 | public class FutureAd { |
09 | public synchronized Ad getAd() { |
17 | } catch (InterruptedException e) {} |
23 | public synchronized void setAd(Ad ad) { |
在Java 5.0以后,Java并发框架(java.util.concurrent)已经内置了Future模式。利用java.util.concurrent包提供的ExecuteService和Future等相关实现使用Future模式,无疑是一种很好的选择,避免了从头开始开发该模式的成本及可能遇到的种种问题。以下利用该框架提供的API改写前面的代码: 01 | ExecutorService exec = Executors.newCachedThreadPool(); |
03 | public void searchService(query) { |
05 | Future<Ad> future = exec.submit( new Callable<Ad>() { |
09 | Ad ad = retrieveAdFromRemoteServer(); |
15 | SearchResult result = findResult(query); |
20 | respond2User(result, ad); |
与Future模式相关的一个多线程设计问题是忙等(busy wait)。仍以前述搜索主线程和广告线程为例,下列代码演示了忙等。 主线程: 01 | public void searchService(query) { |
03 | AdThread adThread = new AdThread(); |
07 | SearchResults results = findResults(query); |
11 | if (!adThread.isDone()) { |
17 | Ad ad = adThread.getAd(); |
19 | mergeResultsAndAd(results, ad); |
广告线程: 01 | public class AdThread extends Thread { |
05 | private boolean done = false ; |
09 | Ad ad = retrieveAdFromRemoteServer(); |
17 | public boolean isDone() { |
这样的代码在生产环境中并不鲜见。忙等至少会造成两个问题:首先,等待的线程会消耗CPU时间,这是没有必要的。再者,等待的线程和被等待的线程会轮流占用CPU,造成不必要的上下文切换(context switch)。这两个问题都会对性能产生影响。此外,在上述代码中,对AdThread类的done实例变量的写操作结果不能保证马上被读操作看见,除非声明成volatile,原因参见java内存模型。 最近在重构老代码的过程中,遇到了很多这样的问题。而忙等的问题可以由Future模式来解决。在Future模式中,通过巧妙的设计来协调线程间通信,避免不必要的上下文切换,改善了性能。
|