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

在Java类库中出现的第一个关联的集合类是 Hashtable ,它是JDK 1.0的一 部分。 Hashtable 提供了一种易于使用的、线程安全的、关联的map功能,这当 然也是方便的。然而,线程安全性是凭代价换来的―― Hashtable 的所有方法 都是同步的。此时,无竞争的同步会导致可观的性能代价。 Hashtable 的后继 者 HashMap 是作为JDK1.2中的集合框架的一部分出现的,它通过提供一个不同 步的基类和一个同步的包装器 Collections.synchronizedMap ,解决了线程安 全性问题。通过将基本的功能从线程安全性中分离开来, Collections.synchronizedMap 允许需要同步的用户可以拥有同步,而不需要同 步的用户则不必为同步付出代价。

Hashtable 和 synchronizedMap 所采取的获得同步的简单方法(同步 Hashtable 中或者同步的 Map 包装器对象中的每个方法)有两个主要的不足。 首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问 hash表。同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然 需要额外的同步。虽然诸如 get() 和 put() 之类的简单操作可以在不需要额外 同步的情况下安全地完成,但还是有一些公用的操作序列,例如迭代或者put- if-absent(空则放入),需要外部的同步,以避免数据争用。

有条件的线程安全性

同步的集合包装器 synchronizedMap 和 synchronizedList ,有时也被称作 有条件地线程安全――所有单个的操作都是线程安全的,但是多个操作组成的操 作序列却可能导致数据争用,因为在操作序列中控制流取决于前面操作的结果。 清单1中第一片段展示了公用的put-if-absent语句块――如果一个条目不在 Map 中,那么添加这个条目。不幸的是,在 containsKey() 方法返回到 put() 方法 被调用这段时间内,可能会有另一个线程也插入一个带有相同键的值。如果您想 确保只有一次插入,您需要用一个对 Map m 进行同步的同步块将这一对语句包 装起来。

清单1中其他的例子与迭代有关。在第一个例子中, List.size() 的结果在 循环的执行期间可能会变得无效,因为另一个线程可以从这个列表中删除条目。 如果时机不得当,在刚好进入循环的最后一次迭代之后有一个条目被另一个线程 删除了,则 List.get() 将返回 null ,而 doSomething() 则很可能会抛出一 个 NullPointerException 异常。那么,采取什么措施才能避免这种情况呢?如 果当您正在迭代一个 List 时另一个线程也可能正在访问这个 List ,那么在进 行迭代时您必须使用一个 synchronized 块将这个 List 包装起来,在 List 1 上同步,从而锁住整个 List 。这样做虽然解决了数据争用问题,但是在并发性 方面付出了更多的代价,因为在迭代期间锁住整个 List 会阻塞其他线程,使它 们在很长一段时间内不能访问这个列表。

集合框架引入了迭代器,用于遍历一个列表或者其他集合,从而优化了对一 个集合中的元素进行迭代的过程。然而,在 java.util 集合类中实现的迭代器 极易崩溃,也就是说,如果在一个线程正在通过一个 Iterator 遍历集合时,另 一个线程也来修改这个集合,那么接下来的 Iterator.hasNext() 或 Iterator.next() 调用将抛出 ConcurrentModificationException 异常。就拿 刚才这个例子来讲,如果想要防止出现 ConcurrentModificationException 异 常,那么当您正在进行迭代时,您必须使用一个在 List l 上同步的 synchronized 块将该 List 包装起来,从而锁住整个 List 。(或者,您也可 以调用 List.toArray() ,在不同步的情况下对数组进行迭代,但是如果列表比 较大的话这样做代价很高)。

清单 1. 同步的map中的公用竞争条件

Map m = Collections.synchronizedMap(new HashMap());
  List l = Collections.synchronizedList(new ArrayList());
  // put-if-absent idiom -- contains a race condition
  // may require external synchronization
  if (!map.containsKey(key))
   map.put(key, value);
  // ad-hoc iteration -- contains race conditions
  // may require external synchronization
  for (int i=0; i<list.size(); i++) {
   doSomething(list.get(i));
  }
  // normal iteration -- can throw ConcurrentModificationException
  // may require external synchronization
  for (Iterator i=list.iterator(); i.hasNext(); ) {
   doSomething(i.next());
  }

信任的错觉

synchronizedList 和 synchronizedMap 提供的有条件的线程安全性也带来 了一个隐患 ―― 开发者会假设,因为这些集合都是同步的,所以它们都是线程 安全的,这样一来他们对于正确地同步混合操作这件事就会疏忽。其结果是尽管 表面上这些程序在负载较轻的时候能够正常工作,但是一旦负载较重,它们就会 开始抛出 NullPointerException 或 ConcurrentModificationException 。

时间: 2024-12-03 04:30:52

Java理论与实践: 并发集合类的相关文章

Java理论与实践: 并发在一定程度上使一切变得简单

当项目中需要 XML 解析器.文本索引程序和搜索引擎.正则表达式编译器. XSL 处理器或 PDF 生成器时,我们中大多数人从不会考虑自己去编写这些实用 程序.每当需要这些设施时,我们会使用商业实现或开放源码实现来执行这些任 务原因很简单 ― 现有实现工作得很好,而且易于使用,自己编写这些实用程序 会事倍功半,或者甚至得不到结果.作为软件工程师,我们更愿意遵循艾萨克 ・牛顿的信念 ― 站在巨人的肩膀之上,有时这是可取的,但并不总是这 样.(在 Richard Hamming 的 Turing A

Java理论与实践专题

Java理论与实践: JDK 5.0中更灵活.更具可伸缩性的锁定机制 Java理论和实践: 一个有缺陷的微基准的剖析 Java理论和实践: 理解JTS ― 平衡安全性和性能 Java理论和实践: 理解JTS ― 幕后魔术 Java理论和实践: 安全构造技术 Java理论与实践: 平衡测试,第3部分:用方面检验设计约束 Java理论与实践:平衡测试,第2部分:编写和优化bug检测器 Java理论与实践:平衡测试,第1部分:不要仅编写测试,还要编写bu Java理论与实践: 您的小数点到哪里去了?

Java 理论与实践:变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用.尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变.在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则.请在附带的论坛中与作者和其他读者分享您关于本文的心得.(也可以单击文章顶部或底部的"讨论"来访问论坛.) 不变对象是指在实例化后其外部可见状态无法更改的对象.Java 类库中的 String.

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

伸缩 内容: synchronized 快速回顾 对 synchronized 的改进 比较 ReentrantLock 和 synchronized 的可伸缩性 条件变量 这不公平 结束语 参考资料 关于作者 对本文的评价 相关内容: Java 理论与实践 系列 Synchronization is not the enemy Reducing contention IBM developer kits for the Java platform (downloads) 订阅: develop

Java理论与实践: 垃圾收集简史

Java 语言可能是使用最广泛的依赖于垃圾收集的编程语言,但是它并不是第 一个.垃圾收集已经成为了包括 Lisp.Smalltalk.Eiffel.Haskell.ML. Scheme和 Modula-3 在内的许多编程语言的一个集成部分,并且从 20 世纪 60 年代早期就开始使用了.在 Java 理论与实践的本篇文章中,Brian Goetz 描述 了垃圾收集最常用的技术. 垃圾收集的好处是无可争辩的 ―― 可靠性提高.使内存管理与类接口设计 分离,并使开发者减少了跟踪内存管理错误的时间.著

Java理论与实践: 构建一个更好的HashMap

ConcurrentHashMap 是 Doug Lea的 util.concurrent 包的一部分,它提供 比Hashtable 或者 synchronizedMap 更高程度的并发性.而且,对于大多数成 功的 get() 操作它会设法避免完全锁定,其结果就是使得并发应用程序有着非 常好的吞吐量.这个月,BrianGoetz 仔细分析了 ConcurrentHashMap的代码, 并探讨 Doug Lea 是如何在不损失线程安全的情况下取得这么骄人成绩的. 在7月份的那期 Java理论与实践

Java理论与实践: 修复Java内存模型,第1部分

活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议.原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile .final 以及 synchronized.在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM.这些更改有些已经 集成在 JDK 1.4 中:而另一些将会包含

Java理论与实践:做个好的(事件)侦听器

观察者模式在 Swing 开发中很常见,在 GUI 应用程序以外的场景中,它对 于消除组件的耦合性也非常有用.但是,仍然存在一些侦听器登记和调用方面的 常见缺陷.在 Java 理论与实践 的这一期中,Java 专家 Brian Goetz 就如何 做一个好的侦听器,以及如何对您的侦听器也友好,提供了一些感觉很好的建议 .请在相应的 讨论论坛 上与作者和其他读者分享您对这篇文章的想法.(您也 可以单击本文顶部或底部的 讨论 访问论坛.) Swing 框架以事件侦听器的形式广泛利用了观察者模式(也称

Java理论和实践: 安全构造技术

Java 语言提供了灵活的.看上去很简单的线程功能,使得您很容易在您的应用程序中使用多线程.然而,Java应用程序中的并发编程比看上去要复杂:在 Java 程序中,有一些微妙(也许并不是那么微妙)方式会造成数据争用(data race)以及并发问题.在这篇 Java 理论和实践中,Brian探讨了一个常见的线程方面的危险:在构造过程中,允许 this 引用逃脱(escape).这个看上去没有什么危害的做法可以在 Java 程序中造成无法可预料和不期望的结果. 测试和调试多线程程序是极其困难的,因