`
jokermanager
  • 浏览: 140443 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

使用不常进行修改的可变集合来减少应用程序的同步开销

阅读更多

使用多个 Java 线程之间共享数据的缺点在于数据访问必须同步,从而避免出现不一致的内容视图,后者可能会导致应用程序失败。例如,Hashtable 类的 put()get() 方法是同步的。因为需要实现同步,所以 put()get() 方法在执行时将同时单独地访问数据;否则,应用程序数据结构可能会被破坏。

当某个应用程序的线程频繁访问这些方法,导致线程出现阻塞时,这些方法的同步点将成为瓶颈。每次只能有一个线程获得内容的访问权。而其他线程必须等 待。如果线程出现排队等候(如果不是这样,线程能够进行其他有用操作),性能和吞吐量将下降。当性能分析显示同步方法实际上会导致排队点时,对代码进行优 化是有益的。

对于很少进行修改的数据,一种被称为分代数据结构(generational data structure) 的技术允许您使用较低的 volatile 开销来安全地发布可变数据结构。当数据结构被频繁访问但很少进行修改时,这将获得性能增益。例如,可以使用未同步的数据结构如 HashMap ,而不是同步的数据结构如 Hashtable 。该技术的关键内容包括:

  1. 发生更新时,制作数据结构的新副本。
  2. 完全填充它。
  3. 使用 volatile 引用将更新安全地发布到所有客户。

使用该技术,getput 操作永远不会在数据结构的同一个示例上同时执行。将确保两个线程不会尝试同时更新数据结构,并且读取线程会始终查看一致的、最新版本的数据。(即使数据被 频繁更新,仍可以使用该方法,不过通过改善并发性而获得的性能增益将损失。频繁地重新填充数据结构可能会抵消由消除同步存取器方法而获得的性能增益。)

 

成对类的适用性

Hashtable 是一种 Java 类,它提供了多线程共享数据的访问。HashMap 在功能上类似于 Hashtable ,但它不是线程安全的。本文所提供的技术适用于其他成对类,它们彼此相似,不同之处在于其中一个类有同步的访问方法,而另一个没有。例如,Vector 有同步的访问方法,而 ArrayList 没有。这两个类都提供相似的功能,而且可以使用本文所讨论的方法。

该技术使用了 Java 语言的三个特性;

  • 自动垃圾收集。 当对象的最后一个引用不再使用时,Java 运行时可以自动释放该对象。应用程序不需要进行其他操作,只需确认当应用程序不再使用某个对象时,没有任何引用指向该对象。早期创建的对象会在最后一个客户使用完成后被自动释放。

  • 对象引用的原子性。 一 个获取对象引用的简单赋值语句是不能被中断的。这意味着只要消费线程可以使用较旧的(但完整的)对象副本生成正确结果,就没有必要围绕单个对象的赋值语句 实现同步。但是,必须注意的是仍需在生产者(producer)线程上采取操作,以确保在执行赋值之前创建完成新的对象。正如本文 讨论 部分所述,生产者线程中需要使用同步代码以确保在对象赋值前完成对象创建。但是,不必在消费者线程中加入同步代码,这将消除开销较大的排队点。

  • Java 内存模型。 Java 内存模型规定了 synchronizedvolatile 的语义。这些规则定义了共享对象及其内容在何时对于除当前正在执行的线程之外的线程是可见的。

为维持两个独立的数据结构实例而对数据结构中的数据进行修改时,您可以使用 Java 语言的以上特性。一旦其中一个被填充,它就不会再次更改。它是有效不可变的 。如果允许 getput 操作在同一个数据结构上同时执行,这是比较危险的。本文所讨论的技术将确保所有 put 操作会在执行任何 get 操作之前完成。

技术

清单 1 中的示例代码阐述了该技术:


清单 1. 避免出现排队点的生产者/消费者代码

 

static   volatile  Map currentMap  =   new  HashMap();  //  this must be volatile to ensure 
                                                
//  consumers will see updated values
static  Object lockbox  =   new  Object();  

public   static   void  buildNewMap()  ... {               //  This is called by the producer   
                                                
//  when the data needs to be updated.  

    
synchronized  (lockbox)  ... {                     //  This must be synchronized because
                                                
//  of the Java memory model.
                                              
      Map newMap 
=   new  HashMap(currentMap);      //  for cases where new data is based on
                                                
//  the existing values, you can use the
                                                
//  currentMap as a starting point.

      
//  add or remove any new or changed items to the newMap
      newMap.put(....);
      newMap.put(....);         
      
      currentMap 
=  newMap;

   }
                 
/**/ /*  After the above synchronization block, everything that is in the HashMap is 
   visible outside this thread.  The updated set of values is available to 
   the consumer threads.  
   
   As long as assignment operation can complete without being interrupted 
   and is guaranteed to be written to shared memory and the consumer can 
   live with the out of date information temporarily, this should work fine. 
*/


  
}

public   static  Object getFromCurrentMap(Object key)  ... {   //  Called by consumer threads.
    

    Map m 
=  currentMap;              //  No locking around this is required.
 
    Object result 
=  m.get(key);      //  get on a HashMap is not synchronized.
    
    
//  Do any additional processing needed using the result.
 
    
return (result);

}

下面将详细讨论清单 1 的内容:

  • 第二个变量 — 即清单 1 中的 newMap — 将保存用数据填充的 HashMap 。这个变量受 synchronized 块的保护,一次只能由一个线程使用 — producer 线程的工作是进行以下操作:

    • 创建新的 HashMap 并将其存储在 newMap 变量中。
    • newMap 上执行整个 put 操作集,这样消费者线程所需的所有数据都包含在 newMap 中。
    • newMap 被完全填充后,将 newMap 的值指定为 currentMap

    由于定时器(或侦听器)会在某些外部数据(如数据库)发生更改时被唤醒,因此可以定期执行生产者线程。

  • 需要使用 currentMap 内容的消费者线程仅仅访问对象并执行 get 操作。请注意 m = currentMap 赋值是一个单元操作,而且不需要进行同步,即使其他线程可能正在访问对象的值。这是安全的,因为 currentMap 是可变的,并且是在生产者的同步块内部进行填充。这意味着通过 currentMap 引用读取的数据结构内容至少会与 currentMap 引用本身保持一致的更新程度。

讨论

一旦将 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 对本文作出的技术评论和建议,使本文更加完整、严谨和准确。

参考资料

学习

分享到:
评论

相关推荐

    C#微软培训资料

    第三章 编写第一个应用程序 .20 3.1 Welcome 程序 .20 3.2 代 码 分 析 .20 3.3 运 行 程 序 .23 .4 添 加 注 释 .25 3.5 小 结 .27 第二部分 C#程序设计基础.28 第四章 数 据 类 型 .28 4.1 值 类 型...

    CLR.via.C#.(中文第3版)(自制详细书签)

    · 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...

    CLR.via.C#.(中文第3版)(自制详细书签)Part2

    · 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...

    CLR.via.C#.(中文第3版)(自制详细书签)Part1

    · 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...

    CLR.via.C#.(中文第3版)(自制详细书签)Part3

    · 使用CLR寄宿、AppDomain、程序集加载、反射和C#的dynamic类型来构造具有动态扩展能力的应用程序 本书作者作者Jeffrey Richter,.NET和Windows编程领域当之无愧的大师和权威,以著述清楚明了,行文流水,言简意赅...

    超级有影响力霸气的Java面试题大全文档

     final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 finally是异常处理语句结构的一部分,表示总是执行。 finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的...

    【Python入门基础】常用数据结构(二)——元组,集合及字典

    事实上在项目中尤其是多线程环境中可能更喜欢使用的是那些不变对象,一方面因为对象状态不能修改,所以可以避免由此引起的不必要的程序错误,简单地说就是一个不变的对象要比可变的对象更加容易维护;另一方面因为...

    疯狂JAVA讲义

    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 ...

    java 面试题 总结

     final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 finally是异常处理语句结构的一部分,表示总是执行。 finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的...

    《计算机操作系统》期末复习指导

    分为固定分区、可变分区、可重定位分区、多重分区。 内存“扩充”技术: •交换:由操作系统做,用户不知道。 •覆盖:由用户控制,操作系统提供覆盖机制。 内存保护技术: ---保护系统工作...

    oracle学习文档 笔记 全面 深刻 详细 通俗易懂 doc word格式 清晰 连接字符串

    varchar2 1~4000字节 可变长度字符串,与CHAR类型相比,使用VARCHAR2可以节省磁盘空间,但查询效率没有char类型高 数值类型 Number(m,n) m(1~38) n(-84~127) 可以存储正数、负数、零、定点数和精度为38位的浮点数...

    《垃圾收集》(Garbage Collection)扫描版[PDF]——part2

    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章 ...

    (Garbage Collection)扫描版——part1

    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章 ...

Global site tag (gtag.js) - Google Analytics