Java理论与实践: 并发在必然水平上使一切变得简朴
副标题#e#
当项目中需要 XML 理会器、文本索引措施和搜索引擎、正则表达式编译器、 XSL 处理惩罚器或 PDF 生成器时,我们中大大都人从不会思量本身去编写这些实用 措施。每当需要这些设施时,我们会利用贸易实现或开放源码实现来执行这些任 务原因很简朴 ― 现有实现事情得很好,并且易于利用,本身编写这些实用措施 会事倍功半,可能甚至得不到功效。作为软件工程师,我们更愿意遵循艾萨克 ・牛顿的信念 ― 站在巨人的肩膀之上,有时这是可取的,但并不老是这 样。(在 Richard Hamming 的 Turing Award 讲座中,他认为计较机科学家的 “自立”要更可取。)
探究反复发现“车轮”之 原因
对付一些险些每个处事器应用措施都需要的初级应用措施框架处事 (如日志记录、数据库毗连适用、高速缓存和任务调治等),我们看到这些根基 的基本布局处事被一遍又一各处重写。为什么会产生这种环境?因为现有的选择 不足充实,可能因为定制版本要更好些或更适合手边的应用措施,但我认为这是 不须要的。事实上,专为某个应用措施开拓的定制版本经常并不比遍及可用的、 通用的实现更适合于该应用措施,也许会更差。譬喻,尽量您不喜欢 log4j,但 它可以完成任务。尽量本身开拓的日志记录系统也许有一些 log4j 所缺乏的特 定特性,但对付大大都应用措施,您很难证明,一个完善的定制日志记录包值得 支付从新编写的价钱,而不利用现有的、通用的实现。但是,很多项目团队最终 照旧本身一遍又一各处编写日志记录、毗连适用或线程调治包。
外貌上 看起来简朴
我们不思量本身去编写 XSL 处理惩罚器的原因之一是,这将耗费 大量的事情。但这些初级的框架处事外貌上看起来简朴,所以本身编写它们好像 并不坚苦。然而,它们很难正常事情,并不象开始看起来那样。这些非凡的 “轮子”一直处在反复发现之中的主要原因是,在给定的应用措施中 ,往往一开始对这些东西的需求很是小,但当您碰着了无数其它项目中也存在的 同样问题时,这种需求会逐渐变大。来由凡是象这样:“我们不需要完善 的日志记录/调治/高速缓存包,只需要一些简朴的包,所以只编写一些能到达 我们目标的包,我们将针对本身特定的需求来调解它”。但环境往往是, 您很快扩展了所编写的这个简朴东西,并试图添加再添加更多的特性,直到编写 出一个完善的基本布局处事。至此,您凡是会执著于本身所编写的措施,无论它 是好是坏。您已经为构建本身的措施支付了全部的价钱,所以除了转至通用的实 现所实际投入的迁移本钱之外,还必需降服这种“已付出本钱”的障 碍。
并发构件的代价地址
编写调治和并发基本布局类简直要比看上去难。Java 语言提供了一组有用的 初级同步原语: wait() 、 notify() 和 synchronized ,但详细利用这些原语 需要一些能力,需要思量机能、死锁、公正性、资源打点以及如何制止线程安详 性方面带来的危害等诸多因素。并发代码难以编写,更难以测试 ― 纵然专家有 时在第一次时也会呈现错误。 Concurrent Programming in Java(请参阅 参考 资料)的作者 Doug Lea 编写了一个极其优秀的、免费的并发实用措施包,它包 括并发应用措施的锁、互斥、行列、线程池、轻量级任务、有效的并发荟萃、原 子的算术操纵和其它根基构件。人们一般称这个包为 util.concurrent (因为 它实际的包名很长),该包将形成 Java Community Process JSR 166 正在尺度 化的 JDK 1.5 中 java.util.concurrent 包的基本。同时, util.concurrent 颠末尾精采的测试,很多处事器应用措施(包罗 JBoss J2EE 应用措施处事器) 都利用这个包。
填补空缺
焦点 Java 类库中略去了一组有用的高级同步东西(譬如互斥、信号和阻塞 、线程安详荟萃类)。Java 语言的并发原语 ― synchronization 、 wait() 和 notify() ― 对付大大都处事器应用措施的需求而言过于初级。假如要试图 获取锁,但假如在给定的时间段内超时了还没有得到它,会产生什么环境?假如 线程间断了,则放弃获取锁的实验?建设一个至多可有 N 个线程持有的锁?支 持多种方法的锁定(譬如带互斥写的并发读)?可能以一种方法来获取锁,但以 另一种方法释放它?内置的锁定机制不直接支持上述这些景象,但可以在 Java 语言所提供的根基并发原语上构建它们。可是这样做需要一些能力,并且容易出 错。
处事器应用措施开拓人员需要简朴的设施来执行互斥、同步事件响应、跨活 动的数据通信以及异步地调治任务。对付这些任务,Java 语言所提供的初级原 语很难用,并且容易堕落。 util.concurrent 包的目标在于通过提供一组用于 锁定、阻塞行列和任务调治的类来填补这项空缺,从而可以或许处理惩罚一些常见的错误 环境可能限制任务行列和运行中的任务所耗损的资源。
#p#副标题#e#
调治异步任务
#p#分页标题#e#
util.concurrent 中利用最遍及的类是那些处理惩罚异步事件调治的类。在本专 栏七月份的文章中,我们研究了 thread pools and work queues,以及很多 Java 应用措施是如何利用“ Runnable 行列”模式调治小事情单位。
可以通过简朴地为某个任务建设一个新线程来派生执行该任务的后端线程, 这种做法很吸引人:
new Thread(new Runnable() { … } ).start();
固然这种做法很好,并且很简捷,但有两个重大缺陷。首先,建设新的线程 需要淹灭必然资源,因此发生出许很多多线程,每个将执行一个简短的任务,然 退却出,这意味着 JVM 也许要做更多的事情,建设和销毁线程而耗损的资源比 实际做有用事情所耗损的资源要多。纵然建设和销毁线程的开销为零,这种执行 模式仍然有第二个更难以办理的缺陷 ― 在执行某类任务时,如何限制所利用的 资源?假如溘然到来大量的请求,如何防备同时会发生大量的线程?现实世界中 的处事器应用措施需要比这更小心地打点资源。您需要限制同时执行异步任务的 数目。
线程池办理了以上两个问题 — 线程池具有可以同时提高调治效率和限制资 源利用的长处。固然人们可以利便地编写事情行列和用池线程执行 Runnable 的 线程池(七月份那篇专栏文章中的示例代码正是用于此目标),但编写有效的任 务调治措施需要做比简朴地同步对共享行列的会见更多的事情。现实世界中的任 务调治措施应该可以处理惩罚死线程,杀死超量的池线程,使它们不用耗不须要的资 源,按照负载动态地打点池的巨细,以及限制列队任务的数目。为了防备处事器 应用措施在过载时由于内存不敷错误而造成瓦解,最后一项(即限制列队的任务 数目)是很重要的。
限制任务行列需要做决定 ― 假如事情行列溢出,则如那里理惩罚这种溢出?抛 弃最新的任务?丢弃最老的任务?阻塞正在提交的线程直到行列有可用的空间? 在正在提交的线程内执行新的任务?存在着各类切实可行的溢出打点计策,每种 计策城市在某些景象下适合,而在另一些景象下不适合。
Executor
Util.concurrent 界说一个 Executor 接口,以异步地执行 Runnable ,另 外还界说了 Executor 的几个实现,它们具有差异的调治特征。将一个任务排入 executor 的行列很是简朴:
Executor executor = new QueuedExecutor();
...
Runnable runnable = ... ;
executor.execute(runnable);
最简朴的实现 ThreadedExecutor 为每个 Runnable 建设了一个新线程,这 里没有提供资源打点 ― 很象 new Thread(new Runnable() {}).start() 这个 常用的要领。但 ThreadedExecutor 有一个重要的长处:通过只改变 executor 布局,就可以转移到其它执行模子,而不必迟钝地在整个应用措施源码内查找所 有建设新线程的处所。 QueuedExecutor 利用一个后端线程来处理惩罚所有任务,这 很是雷同于 AWT 和 Swing 中的事件线程。 QueuedExecutor 具有一个很好的特 性:任务凭据列队的顺序来执行,因为是在一个线程内来执行所有的任务,任务 无需同步对共享数据的所有会见。
PooledExecutor 是一个巨大的线程池实现,它不单提供事情线程(worker thread)池中任务的调治,并且还可机动地调解池的巨细,同时还提供了线程生 命周期打点,这个实现可以限制事情行列中任务的数目,以防备行列中的任务耗 尽所有可用内存,别的还提供了多种可用的封锁和饱和度计策(阻塞、废弃、抛 出、废弃最老的、在挪用者中运行等)。所有的 Executor 实现为您打点线程的 建设和销毁,包罗当封锁 executor 时,封锁所有线程,别的还为线程建设进程 提供了 hook,以便应用措施可以打点它但愿打点的线程实例化。譬喻,这使您 可以将所有事情线程放在特定的 ThreadGroup 中,可能赋予它们描写性名称。
FutureResult
有时您但愿异步地启动一个历程,同时但愿在今后需要这个历程时,可以使 用该历程的功效。 FutureResult 实用措施类使这变得很容易。 FutureResult 暗示大概要花一段时间执行的任务,而且可以在另一个线程中执行此任务, FutureResult 工具可用作执行历程的句柄。通过它,您可以查明该任务是否已 经完成,可以期待任务完成,并检索其功效。可以将 FutureResult 与 Executor 组合起来;可以建设一个 FutureResult 并将其排入 executor 的队 列,同时保存对 FutureResult 的引用。清单 1 显示了一个一同利用 FutureResult 和 Executor 的简朴示例,它异步地启动图像着色,并继承举办 其它处理惩罚:
清单 1. 运作中的 FutureResult 和 Executor
#p#分页标题#e#
Executor executor = ...
ImageRenderer renderer = ...
FutureResult futureImage = new FutureResult();
Runnable command = futureImage.setter(new Callable() {
public Object call() { return renderer.render(rawImage); }
});
// start the rendering process
executor.execute(command);
// do other things while executing
drawBorders();
drawCaption();
// retrieve the future result, blocking if necessary
drawImage((Image)(futureImage.get())); // use future
FutureResult 和高速缓存
还可以利用 FutureResult 来提高按需装入高速缓存的并发性。通过将 FutureResult 安排在高速缓存内,而不是安排计较自己的功效,可以淘汰持有 高速缓存上写锁的时间。固然这种做法不能加速第一个线程把某一项放入高速缓 存,但它 将淘汰第一个线程阻塞其它线程会见高速缓存的时间。它还使其它线 程更早地利用功效,因为它们可以从高速缓存中检索 FutureTask 。清单 2 显 示了利用用于高速缓存的 FutureResult 示例:
清单 2. 利用 FutureResult 来改进高速缓存
public class FileCache {
private Map cache = new HashMap();
private Executor executor = new PooledExecutor();
public void get(final String name) {
FutureResult result;
synchronized(cache) {
result = cache.get(name);
if (result == null) {
result = new FutureResult();
executor.execute(result.setter(new Callable() {
public Object call() { return loadFile(name); }
}));
cache.put(result);
}
}
return result.get();
}
}
这种要领使第一个线程快速地进入和退出同步块,使其它线程与第一个线程 一样快地获得第一个线程计较的功效,不行能呈现两个线程都试图计较同一个对 象。
竣事语
util.concurrent 包包括很多有用的类,您大概认为个中一些类与您已编写 的类一样好,也许甚至比从前还要好。它们是很多多线程应用措施的根基构件的 高机能实现,并经验了大量测试。 util.concurrent 是 JSR 166 的切入点,它 将带来一组并发性的实用措施,这些实用措施将成为 JDK 1.5 中的 java.util.concurrent 包,但您不必比及当时侯才气利用它。在今后的文章中 ,我将接头 util.concurrent 中一些定制的同步类,并研究 util.concurrent 和 java.util.concurrent API 中的差异之处。