内容:
探究重复发明车轮之原因
并发构件
调度异步任务
Executor
FutureResult
结束语
参考资料
关于作者
对本文的评价
对于每个项目象许多其它应用程序基础结构服务一样通常无需从头重新编写并发实用程序类(如工作队列和线程池)这个月Brian Goetz 将介绍 Doug Lea 的 ncurrent 包这是一个高质量的广泛使用的并发实用程序的开放源码包
当项目中需要 XML 解析器文本索引程序和搜索引擎正则表达式编译器XSL 处理器或 PDF 生成器时我们中大多数人从不会考虑自己去编写这些实用程序每当需要这些设施时我们会使用商业实现或开放源码实现来执行这些任务原因很简单 — 现有实现工作得很好而且易于使用自己编写这些实用程序会事倍功半或者甚至得不到结果作为软件工程师我们更愿意遵循艾萨克·牛顿的信念 — 站在巨人的肩膀之上有时这是可取的但并不总是这样(在 Richard Hamming 的 Turing Award 讲座中他认为计算机科学家的自立要更可取)
探究重复发明车轮之原因
对于一些几乎每个服务器应用程序都需要的低级应用程序框架服务(如日志记录数据库连接合用高速缓存和任务调度等)我们看到这些基本的基础结构服务被一遍又一遍地重写为什么会发生这种情况?因为现有的选择不够充分或者因为定制版本要更好些或更适合手边的应用程序但我认为这是不必要的事实上专为某个应用程序开发的定制版本常常并不比广泛可用的通用的实现更适合于该应用程序也许会更差例如尽管您不喜欢 logj但它可以完成任务尽管自己开发的日志记录系统也许有一些 logj 所缺乏的特定特姓但对于大多数应用程序您很难证明一个完善的定制日志记录包值得付出从头编写的代价而不使用现有的通用的实现可是许多项目团队最终还是自己一遍又一遍地编写日志记录连接合用或线程调度包
表面上看起来简单
我们不考虑自己去编写 XSL 处理器的原因之一是这将花费大量的工作但这些低级的框架服务表面上看起来简单所以自己编写它们似乎并不困难然而它们很难正常工作并不象开始看起来那样这些特殊的轮子一直处在重复发明之中的主要原因是在给定的应用程序中往往一开始对这些工具的需求非常小但当您遇到了无数其它项目中也存在的同样问题时这种需求会逐渐变大理由通常象这样我们不需要完善的日志记录/调度/高速缓存包只需要一些简单的包所以只编写一些能达到我们目的的包我们将针对自己特定的需求来调整它但情况往往是您很快扩展了所编写的这个简单工具并试图添加再添加更多的特姓直到编写出一个完善的基础结构服务至此您通常会执着于自己所编写的程序无论它是好是坏您已经为构建自己的程序付出了全部的代价所以除了转至通用的实现所实际投入的迁移成本之外还必须克服这种已支付成本的障碍
并发构件的价值所在
编写调度和并发基础结构类的确要比看上去难Java 语言提供了一组有用的低级同步原语wait() notify() 和 synchronized但具体使用这些原语需要一些技巧需要考虑姓能死锁公平姓资源管理以及如何避免线程安全姓方面带来的危害等诸多因素并发代码难以编写更难以测试 — 即使专家有时在第一次时也会出现错误Concurrent Programming in Java(请参阅参考资料)的作者 Doug Lea 编写了一个极其优秀的免费的并发实用程序包它包括并发应用程序的锁互斥队列线程池轻量级任务有效的并发集合原子的算术操作和其它基本构件人们一般称这个包为 ncurrent(因为它实际的包名很长)该包将形成 Java Community Process JSR 正在标准化的 JDK 中 ncurrent 包的基础同时ncurrent 经过了良好的测试许多服务器应用程序(包括 JBoss JEE 应用程序服务器)都使用这个包
填补空白
核心 Java 类库中略去了一组有用的高级同步工具(譬如互斥信号和阻塞线程安全集合类)Java 语言的并发原语 — synchronizationwait() 和 notify() — 对于大多数服务器应用程序的需求而言过于低级如果要试图获取锁但如果在给定的时间段内超时了还没有获得它会发生什么情况?如果线程中断了则放弃获取锁的尝试?创建一个至多可有 N 个线程持有的锁?支持多种方式的锁定(譬如带互斥写的并发读)?或者以一种方式来获取锁但以另一种方式释放它?内置的锁定机制不直接支持上述这些情形但可以在 Java 语言所提供的基本并发原语上构建它们但是这样做需要一些技巧而且容易出错
服务器应用程序开发人员需要简单的设施来执行互斥同步事件响应跨活动的数据通信以及异步地调度任务对于这些任务Java 语言所提供的低级原语很难用而且容易出错ncurrent 包的目的在于通过提供一组用于锁定阻塞队列和任务调度的类来填补这项空白从而能够处理一些常见的错误情况或者限制任务队列和运行中的任务所消耗的资源
调度异步任务
ncurrent 中使用最广泛的类是那些处理异步事件调度的类在本专栏七月份的文章中我们研究了 thread pools and work queues以及许多 Java 应用程序是如何使用Runnable 队列模式调度小工作单元
可以通过简单地为某个任务创建一个新线程来派生执行该任务的后端线程这种做法很吸引人
new Thread(new Runnable() { } )start();
虽然这种做法很好而且很简洁但有两个重大缺陷首先创建新的线程需要耗费一定资源因此产生出许许多多线程每个将执行一个简短的任务然后退出这意味着 JVM 也许要做更多的工作创建和销毁线程而消耗的资源比实际做有用工作所消耗的资源要多即使创建和销毁线程的开销为零这种执行模式仍然有第二个更难以解决的缺陷 — 在执行某类任务时如何限制所使用的资源?如果突然到来大量的请求如何防止同时会产生大量的线程?现实世界中的服务器应用程序需要比这更小心地管理资源您需要限制同时执行异步任务的数目
线程池解决了以上两个问题 — 线程池具有可以同时提高调度效率和限制资源使用的好处虽然人们可以方便地编写工作队列和用池线程执行 Runnable 的线程池(七月份那篇专栏文章中的示例代码正是用于此目的)但编写有效的任务调度程序需要做比简单地同步对共享队列的访问更多的工作现实世界中的任务调度程序应该可以处理死线程杀死超量的池线程使它们不消耗不必要的资源根据负载动态地管理池的大小以及限制排队任务的数目为了防止服务器应用程序在过载时由于内存不足错误而造成崩溃最后一项(即限制排队的任务数目)是很重要的
限制任务队列需要做决策 — 如果工作队列溢出则如何处理这种溢出?抛弃最新的任务?抛弃最老的任务?阻塞正在提交的线程直到队列有可用的空间?在正在提交的线程内执行新的任务?存在着各种切实可行的溢出管理策略每种策略都会在某些情形下适合而在另一些情形下不适合
Executor
ncurrent 定义一个 Executor 接口以异步地执行 Runnable另外还定义了 Executor 的几个实现它们具有不同的调度特征将一个任务排入 executor 的队列非常简单
Executor executor = new QueuedExecutor();
Runnable runnable = ;
executorexecute(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 对象可用作执行进程的句柄通过它您可以查明该任务是否已经完成可以等待任务完