util.concurrent
包简介

对于每个项目,象许多其它应用程序基础结构服务一样,通常无需从头重新编写并发实用程序类(如工作队列和线程池)。这个月,Brian Goetz 将介绍 Doug Lea 的 util.concurrent包,这是一个高质量的、广泛使用的、并发实用程序的开放源码包。可以通过本文的 论坛提出您对本文的想法,以飨笔者和其他读者。(您也可以单击本文顶部或底部的“讨论”参加论坛。)

0 评论:

Brian
Goetz
 (brian@quiotix.com),
首席顾问, Quiotix Corp

2003 年 4 月 26 日

  • 内容

当项目中需要 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 包的目的在于通过提供一组用于锁定、阻塞队列和任务调度的类来填补这项空白,从而能够处理一些常见的错误情况或者限制任务队列和运行中的任务所消耗的资源。

回页首

调度异步任务

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
  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
中的不同之处。

参考资料

Java 理论与实践: 并发在一定程度上使一切变得简单的更多相关文章

  1. Java 理论与实践: 并发集合类

    Java 理论与实践: 并发集合类 DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含了一些主要集合类型 List 和 Map 的高性能的.线程安全的实现 ...

  2. Java 理论与实践: 非阻塞算法简介——看吧,没有锁定!(转载)

    简介: Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能.非阻塞算法属于并发算法,它们可以安全地派生它们的线程, ...

  3. Java 理论与实践: 流行的原子——新原子类是 java.util.concurrent 的隐藏精华(转载)

    简介: 在 JDK 5.0 之前,如果不使用本机代码,就不能用 Java 语言编写无等待.无锁定的算法.在 java.util.concurrent 中添加原子变量类之后,这种情况发生了变化.请跟随并 ...

  4. Java 理论与实践: 修复 Java 内存模型,第 2 部分(转载)

    在 JSR 133 中 JMM 会有什么改变? 活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型(Java Memory Model, JMM)的公开建议.在本系列文章的 ...

  5. Java 理论与实践: 流行的原子

    Java 理论与实践: 流行的原子 新原子类是 java.util.concurrent 的隐藏精华 在 JDK 5.0 之前,如果不使用本机代码,就不能用 Java 语言编写无等待.无锁定的算法.在 ...

  6. Java 理论与实践: 处理 InterruptedException

    捕捉到它,然后怎么处理它? 很多 Java™ 语言方法,例如 Thread.sleep() 和 Object.wait(),都可以抛出InterruptedException.您不能忽略这个异常,因为 ...

  7. Java 理论与实践: 处理 InterruptedException(转)

    很多 Java™ 语言方法,例如 Thread.sleep() 和 Object.wait(),都可以抛出InterruptedException.您不能忽略这个异常,因为它是一个检查异常(check ...

  8. Java 理论和实践: 了解泛型

    转载自 : http://www.ibm.com/developerworks/cn/java/j-jtp01255.html 表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型) ...

  9. Java 理论与实践: 用弱引用堵住内存泄漏

    弱引用使得表达对象生命周期关系变得容易了 虽然用 Java™ 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集.本月,负责保障应用程序健 ...

  10. Java 理论和实践: 了解泛型 识别和避免学习使用泛型过程中的陷阱

    Brian Goetz (brian@quiotix.com), 首席顾问, Quiotix 简介: JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进.但是,对于初次使用泛 ...

随机推荐

  1. python3实现url全编码/解码

    最近在学习SQL注入,绕过方法中有编码注入绕过,需要将关键词进行全编码,百度了一下没有找到全编码工具,所有的编码工具里"and"编码完还是"and",于是查了一 ...

  2. Docker学习系列3:常用命令之容器命令

    本文是Docker学习系列教程中的第三篇.前几篇教程如下: 「图文教程」Windows11下安装Docker Desktop 「填坑」在windows系统下安装Docker Desktop后迁移镜像位 ...

  3. Python 潮流周刊#68:2023 年 Python 开发者调查结果(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  4. Angular Material 18+ 高级教程 – Material Tooltip

    前言 一个常见的 Tooltip 使用场景是 当有 ellipsis 时,hover 显示全文. Tooltip 算是一种 Popover,我们之前有讲过,要搞 Popover 可以使用底层的 CDK ...

  5. Google Maps Embed API & JavaScript API

    前言 很多年前写过一篇 Google Map 谷歌地图, 这篇算是翻新版本. Google Map Registration Google Maps Platform 是整个 Google Map 的 ...

  6. Go runtime 调度器精讲(九):系统调用引起的抢占

    原创文章,欢迎转载,转载请注明出处,谢谢. 0. 前言 第八讲介绍了当 goroutine 运行时间过长会被抢占的情况.这一讲继续看 goroutine 执行系统调用时间过长的抢占. 1. 系统调用时 ...

  7. storybook 7.6

    https://storybook.js.org/tutorials/intro-to-storybook/vue/zh-CN/get-started/ 开始吧 注释:degit 从 github 拉 ...

  8. 一款超级给力的弱网测试神器—Qnet(附视频)

    一.APP弱网测试背景 App在使用的过程中,难免会遇到不同的弱网络环境,像在公车上.在地铁.地下车库等.在这种情况下,手机常常会出现网络抖动.上行或下行超时,导致APP应用中出现丢包延迟,从而影响用 ...

  9. 更新预警(bushi)

    一回首,上次更新已经是将近3个月前了.但是博主不是似了,也不是逍遥快活游山玩水纸醉金迷乐不思蜀,而是上班太忙还是单休,所以没什么时间更新博客.但是今天我要开始忏悔了!预计更新以下几个专题(不一定真的会 ...

  10. Guava中的Joiner和Splitter

    目录 Guava 介绍 Joiner list转string map转string 处理嵌套集合 处理null值 Splitter string转list string转map 多个拆分符 输出 代码 ...