使用多个 Java 线程之间共享数据的缺点在于数据访问必须同步,从而避免出现不一致的内容视图,后者可能会导致应用程序失败。例如,Hashtable
类的 put()
和 get()
方法是同步的。因为需要实现同步,所以 put()
和 get()
方法在执行时将同时单独地访问数据;否则,应用程序数据结构可能会被破坏。
当某个应用程序的线程频繁访问这些方法,导致线程出现阻塞时,这些方法的同步点将成为瓶颈。每次只能有一个线程获得内容的访问权。而其他线程必须等待。如果线程出现排队等候(如果不是这样,线程能够进行其他有用操作),性能和吞吐量将下降。当性能分析显示同步方法实际上会导致排队点时,对代码进行优化是有益的。
对于很少进行修改的数据,一种被称为分代数据结构(generational data structure)的技术允许您使用较低的 volatile
开销来安全地发布可变数据结构。当数据结构被频繁访问但很少进行修改时,这将获得性能增益。例如,可以使用未同步的数据结构如 HashMap
,而不是同步的数据结构如 Hashtable
。该技术的关键内容包括:
- 发生更新时,制作数据结构的新副本。
- 完全填充它。
- 使用
volatile
引用将更新安全地发布到所有客户。
使用该技术,get
和 put
操作永远不会在数据结构的同一个示例上同时执行。将确保两个线程不会尝试同时更新数据结构,并且读取线程会始终查看一致的、最新版本的数据。(即使数据被频繁更新,仍可以使用该方法,不过通过改善并发性而获得的性能增益将损失。频繁地重新填充数据结构可能会抵消由消除同步存取器方法而获得的性能增益。)
|
成对类的适用性
Hashtable 是一种 Java 类,它提供了多线程共享数据的访问。HashMap 在功能上类似于 Hashtable ,但它不是线程安全的。本文所提供的技术适用于其他成对类,它们彼此相似,不同之处在于其中一个类有同步的访问方法,而另一个没有。例如,Vector 有同步的访问方法,而 ArrayList 没有。这两个类都提供相似的功能,而且可以使用本文所讨论的方法。
|
|
该技术使用了 Java 语言的三个特性;
-
自动垃圾收集。当对象的最后一个引用不再使用时,Java 运行时可以自动释放该对象。应用程序不需要进行其他操作,只需确认当应用程序不再使用某个对象时,没有任何引用指向该对象。早期创建的对象会在最后一个客户使用完成后被自动释放。
-
对象引用的原子性。一个获取对象引用的简单赋值语句是不能被中断的。这意味着只要消费线程可以使用较旧的(但完整的)对象副本生成正确结果,就没有必要围绕单个对象的赋值语句实现同步。但是,必须注意的是仍需在生产者(producer)线程上采取操作,以确保在执行赋值之前创建完成新的对象。正如本文 讨论 部分所述,生产者线程中需要使用同步代码以确保在对象赋值前完成对象创建。但是,不必在消费者线程中加入同步代码,这将消除开销较大的排队点。
-
Java 内存模型。Java 内存模型规定了
synchronized
和 volatile
的语义。这些规则定义了共享对象及其内容在何时对于除当前正在执行的线程之外的线程是可见的。
为维持两个独立的数据结构实例而对数据结构中的数据进行修改时,您可以使用 Java 语言的以上特性。一旦其中一个被填充,它就不会再次更改。它是有效不可变的。如果允许 get
和 put
操作在同一个数据结构上同时执行,这是比较危险的。本文所讨论的技术将确保所有 put
操作会在执行任何 get
操作之前完成。
技术
清单 1 中的示例代码阐述了该技术:
清单 1. 避免出现排队点的生产者/消费者代码
staticvolatileMapcurrentMap=newHashMap();//thismustbevolatiletoensure
//consumerswillseeupdatedvalues
staticObjectlockbox=newObject();
publicstaticvoidbuildNewMap()...{//Thisiscalledbytheproducer
//whenthedataneedstobeupdated.
synchronized(lockbox)...{//Thismustbesynchronizedbecause
//oftheJavamemorymodel.
MapnewMap=newHashMap(currentMap);//forcaseswherenewdataisbasedon
//theexistingvalues,youcanusethe
//currentMapasastartingpoint.
//addorremoveanyneworchangeditemstothenewMap
newMap.put(....);
newMap.put(....);
currentMap=newMap;
}
/**//*Aftertheabovesynchronizationblock,everythingthatisintheHashMapis
visibleoutsidethisthread.Theupdatedsetofvaluesisavailableto
theconsumerthreads.
Aslongasassignmentoperationcancompletewithoutbeinginterrupted
andisguaranteedtobewrittentosharedmemoryandtheconsumercan
livewiththeoutofdateinformationtemporarily,thisshouldworkfine.*/
}
publicstaticObjectgetFromCurrentMap(Objectkey)...{//Calledbyconsumerthreads.
Mapm=currentMap;//Nolockingaroundthisisrequired.
Objectresult=m.get(key);//getonaHashMapisnotsynchronized.
//Doanyadditionalprocessingneededusingtheresult.
return(result);
}
下面将详细讨论清单 1 的内容:
讨论
一旦将 newMap
指定为 currentMap
,则内容始终不会更改。实际上,HashMap
是不可变的。这将允许多个 get
操作并行运行,从而获得主要性能改善。根据 Brian Goetz 在 Java Concurrency in Practice(参阅 参考资料)中 3.5.4 节的论述,即 “无需额外的同步即可使用安全发布的有效不可变对象”,安全发布是 volatile
引用的结果。
读取数据时,惟一可能发生更改的就是 currentMap
变量的对象引用。在消费者线程访问某个值的同时,生产者线程将使用新值覆盖当前值。因为对象引用是 Java 语言中的单元操作,所以在访问该对象时,消费者线程没有必要进行同步。最糟糕情形可能是消费者线程获得 currentMap
引用,然后生产者线程使用较新的内容覆盖该引用。在这种情况下,消费者线程会使用稍微有些旧但仍保持内部一致的数据。如果消费者线程在生产者线程准备运行的前一秒执行,则会出现同样结果。通常,这样不会引起任何问题。关键在于 currentMap
的内容会在发布时始终保持完全一致和不可变。
发生这种竞争时,消费者线程可能会使用 “旧” 版本数据的引用。“新” 的对象引用已经覆盖旧版本,但某些消费者线程仍使用旧版本。当最后一个消费者线程不再使用对旧对象的引用后,该对象将被释放并进行垃圾收集。Java 运行时将记录何时发生上述操作。应用程序不必显式释放旧对象,因为对象释放是自动进行的。
可以基于应用程序的需要,定期创建新版的 currentMap
。按照上述步骤进行操作,可以确保这些更新能够安全地反复进行。
清单 1 中的 synchronized
块必需确保两个生产者线程不会同时竞争更新 currentMap
。那样可能会导致数据损失,从而导致消费者线程查看不确定的结果。synchronized
将阻止优化程序作出这种决策,实际上是将整个映射创建作为原子操作处理。关键字 volatile
可以保证消费者线程在 currentMap
变量的值发生更改后不会继续查看其旧值。更重要的是,可以确保客户通过取消引用对象引用而获得的值至少与引用本身保持一致的更新程度。而普通的引用不能提供这种有序保证。
使用 synchronized
块和 volatile
关键字所带来的影响是消费者线程可以查看一致的视图。数据结构在发布后不会被修改这一事实将为生产者线程提供帮助。在这种情形中 — 发布有效不可变的对象图形 — 所需做的事情就是安全地发布根对象引用。请注意,也可以对根对象引用的消费者访问进行同步,但这将成为排队点,而排队点正是该技术试图避免的。Brian Goetz 将这种方法称为 “开销较低的读-写锁” 技巧(参阅 参考资料)。
结束语
本文所讨论的技术适用于共享数据很少更改且由多线程同时访问的场合。不过该技术仅适用于应用程序不要求 使用绝对最新数据的场合。
最终结果是并发访问随时间变化的共享数据。在要求高并发性的环境中。该技术可以避免在应用程序内部包含不必要的排队点。
需要注意的是由于 Java 内存模型的复杂性,本文所讨论的技术仅用于 Java 5.0 及更高版本。在早期的 Java 版本中,客户机应用程序面临的风险是查看未被完全填充的 HashMap
,或 HashMap
的已破坏的、无效的或不一致的内部数据结构视图。
致谢
本文作者非常感谢 Brian Goetz 对本文作出的技术评论和建议,使本文更加完整、严谨和准确。
参考资料
学习
分享到:
相关推荐
第三章 编写第一个应用程序 .20 3.1 Welcome 程序 .20 3.2 代 码 分 析 .20 3.3 运 行 程 序 .23 .4 添 加 注 释 .25 3.5 小 结 .27 第二部分 C#程序设计基础.28 第四章 数 据 类 型 .28 4.1 值 类 型...
· 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...
· 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...
· 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...
· 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...
final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 finally是异常处理语句结构的一部分,表示总是执行。 finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的...
事实上在项目中尤其是多线程环境中可能更喜欢使用的是那些不变对象,一方面因为对象状态不能修改,所以可以避免由此引起的不必要的程序错误,简单地说就是一个不变的对象要比可变的对象更加容易维护;另一方面因为...
7.8.4 设置不可变集合 288 7.9 烦琐的接口:Enumeration 289 7.10 本章小结 290 本章练习 290 第8章 泛型 291 8.1 泛型入门 292 8.1.1 编译时不检查类型的异常 292 8.1.2 手动实现编译时检查类型 293 8.1.3 ...
final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 finally是异常处理语句结构的一部分,表示总是执行。 finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的...
分为固定分区、可变分区、可重定位分区、多重分区。 内存“扩充”技术: •交换:由操作系统做,用户不知道。 •覆盖:由用户控制,操作系统提供覆盖机制。 内存保护技术: ---保护系统工作...
varchar2 1~4000字节 可变长度字符串,与CHAR类型相比,使用VARCHAR2可以节省磁盘空间,但查询效率没有char类型高 数值类型 Number(m,n) m(1~38) n(-84~127) 可以存储正数、负数、零、定点数和精度为38位的浮点数...
4.3.2 可变大小节点的指针反转 4.3.3 指针反转的开销 4.4 位图标记 4.5 延迟清扫 4.5.1 Hughes的延迟清扫算法 4.5.2 Boehm-Demers-Weriser清扫器 4.5.3 Zorn的延迟清扫器 4.6 需要考虑的问题 4.7 引文注记 第5章 ...
4.3.2 可变大小节点的指针反转 4.3.3 指针反转的开销 4.4 位图标记 4.5 延迟清扫 4.5.1 Hughes的延迟清扫算法 4.5.2 Boehm-Demers-Weriser清扫器 4.5.3 Zorn的延迟清扫器 4.6 需要考虑的问题 4.7 引文注记 第5章 ...