`
jokermanager
  • 浏览: 143844 次
  • 性别: 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 对本文作出的技术评论和建议,使本文更加完整、严谨和准确。

参考资料

学习

分享到:
评论

相关推荐

    不可变对象ImmutableXXX:Collection、List、Set、Map…

    总之,Guava的不可变集合类提供了一种高效、安全的方式来处理集合数据,它们在Java编程中起到了重要的作用,特别是在多线程和高并发场景下。通过合理地使用这些类,开发者可以编写出更简洁、更安全的代码。

    不可变:Crystal语言的线程安全的,持久的,不可变的集合

    这种特性使得不可变集合在多线程环境下尤其有价值,因为它们天生就具有线程安全性,无需额外的同步措施来保证并发访问的安全。在本文中,我们将深入探讨Crystal中的不可变集合,包括它们的设计原理、实现方式以及...

    用ADO.NET2.0进行数据库应用程序开发

    ADO.NET 2.0是微软.NET Framework的一部分,它为开发者提供了高效、灵活的数据库访问机制,使得构建数据库驱动的应用程序变得更加简单。在本教程中,我们将深入探讨如何利用ADO.NET 2.0进行数据库应用程序开发。 1....

    Java基础:减少对象的创建提高java性能

    不可变对象可以安全地在多个线程间共享,无需担心同步问题,这在多线程环境中可以提高效率。此外,由于它们的状态不可改变,JVM有时可以对不可变对象进行优化,如缓存或共享相同的实例,称为“字符串池”。 对于...

    数组、集合对象和范型

    在C#编程中,数组、集合对象和泛型是核心概念,它们在处理数据和构建高效应用程序时扮演着重要角色。 首先,数组是最基础的数据结构,用于存储同一类型的多个元素。在C#中,数组是一种固定大小的内存块,可以一次性...

    Java面试问题2023集合

    12. **不可变集合**:`Collections.unmodifiableList()`、`Collections.unmodifiableSet()`和`Collections.unmodifiableMap()`等方法创建不可变集合,防止集合内容被修改,提高代码安全性。 13. **枚举Set**:`...

    java编程中'为了性能'一些尽量做到的地方

    字符串在Java中是不可变的,每次修改都会生成新的对象。避免使用`+`操作符连接字符串,尤其是在循环中。可以使用StringBuilder或StringBuffer(线程安全)进行拼接。 4. **使用局部变量** 尽可能使用局部变量,...

    JAVA基础-集合

    字符串在JAVA中被视为一个不可变的对象,由`java.lang.String`类表示。这意味着一旦一个字符串被创建,它的值就不能被修改。例如,`String str = "abc";` 和 `String str1 = new String("abc");` 这两种创建字符串的...

    构建高性能的J2EE分布式Web应用系统.pdf

    - 避免频繁改变对象状态,特别是不可变对象(如String),可以通过创建新对象来替代。 - 合理分配对象空间,避免自动扩容带来的性能损耗。 - 对于短生命周期的对象,可以采用对象池机制来提高性能。 - 在对象的...

    immutable-built-in-collections:内置集合的一成不变的变体

    1. **创建**:在`Immutable.js`中,我们可以使用`List.of()`、`Map.of()`等方法创建不可变集合。 2. **访问**:通过索引或键来获取值,不会改变原有集合。 3. **更新**:如果需要修改值,如替换数组中的一个元素,...

    .net性能优化宝典

    这是因为字符串在.NET中是不可变的,每次连接都会创建一个新的字符串对象,而`StringBuilder`则是在内部进行修改,不会产生额外的对象。 **1.2.2 避免不必要的调用ToUpper或ToLower方法** 对字符串进行大小写转换...

    Python库 | frozendict-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl

    6. **并发安全**:在多线程或多进程环境中,不可变数据结构可以减少同步开销,因为它们天生就线程安全。 安装`frozendict`库,你可以使用`pip`命令,如: ```bash pip install frozendict-2.1.2-cp38-cp38-macosx_...

    35个Java代码性能优化总结.pdf

    在处理字符串时,应尽可能使用StringBuilder或StringBuffer来代替String,因为String是不可变的,每次操作都会产生新的String对象。 #### 3. 集合框架使用 在集合框架中,ArrayList和LinkedList的选择至关重要。...

    写了一个简单的数据库连接池 与 大家交流一下,为什么使用Vector呢?

    总之,选择`Vector`可能是出于对线程安全的需求,但在现代Java编程中,考虑到性能和最佳实践,我们可能会推荐使用其他更适合的并发集合类,或者结合`Collections.synchronizedList()`等工具来手动同步`ArrayList`。...

    书---Java并发编程的艺术

    - **不可变对象**:不可变对象一旦被创建之后就不能再修改其状态,因此它们天然具有线程安全性。 - **使用枚举类型**:枚举类型是不可变的,并且Java语言规范保证了枚举类型的单例性,是非常好的不可变对象实例。 #...

    java代码优化细节总结1.0版本.zip

    - 字符串是不可变的,频繁拼接会导致不必要的内存创建,使用`StringBuilder`或`StringBuffer`类。 - 使用`String.equals()`而不是`==`来比较字符串内容,避免空指针异常。 - 利用`String.intern()`方法共享相同的...

    java代码效率优化方法(推荐)

    2. **对象重用**:避免频繁创建对象,尤其是对于像`String`这样的不可变对象。在字符串连接操作中,使用`StringBuilder`或`StringBuffer`代替`+`操作符,因为它们在内部维护一个缓冲区,性能更优。 3. **利用局部...

Global site tag (gtag.js) - Google Analytics