使用在多个Java线程之间共享的数据的一个缺点是必须同步对数据的访问,以避免内容的不一致视图。 例如, Hashtable类的put()和get()方法是同步的。 需要同步,因此执行时,同时put()和get()方法可以唯一访问数据。
当应用程序的线程过于频繁地访问那些方法时,围绕这些方法的同步点可能会成为瓶颈。 一次只能访问一个线程。 其他线程必须等待,这会影响性能和吞吐量。
对于不经常更改的数据,我们开发了一种允许使用HashMap而不是Hashtable 。 在HashMap , get()和put()方法不同步。 开发人员负责确保get和put操作永远不会同时执行。 我们的技术为您提供了一种方法。 (即使频繁更新数据,该方法仍然有效,但是却失去了性能优势。重新填充HashMap抵消通过避免同步访问器方法获得的性能。)
Hashtable是可访问由多个线程共享的数据的许多Java类之一。 此处介绍的技术适用于彼此相似的其他成对的类,除了一个类具有同步的访问器方法,而另一类则没有。 例如, Vector具有同步的访问器,而ArrayList没有。 两者都提供类似的功能,并且可以使用此处描述的方法。
该技术利用了Java语言的其他两个特征:
自动垃圾收集 -当对对象的最后一个引用消失时,Java运行时可以自动释放对象。 除了确保在使用对象完成应用程序时确保没有对象引用外,应用程序无需执行任何操作。 对象引用的原子性 -可以中断访问对象的简单赋值语句。 因此,不必围绕单对象分配语句进行同步。 如果不存在同步点,则排除排队访问对象的可能性。当包含在列表类型对象中的数据确实发生更改时,可以通过保留对象的两个单独实例来利用这些Java语言特性。 一旦填充,它就不会再改变。 它实际上是不可变的 。 如果允许get和put操作同时执行,将很危险。 我们在此介绍的技术可确保在执行任何get之前,所有put完整。
清单1中的示例代码说明了该技术。
清单1中发生了什么:
类变量指向完全填充的HashMap的当前实时版本。 该变量(在清单1中称为currentMap的初始值为null 。 如果get需要读取从对象currentMap时,它有一个null值时, get应该稍后重试。第二个变量(在清单1中称为newMap保存正在填充数据的HashMap 。 一次仅一个线程使用此变量- 生产者线程的工作是:
创建一个新的HashMap并将其存储在newMap变量中。 在newMap上执行一套完整的put操作,以使使用者线程所需的所有数据都在newMap 。 当newMap完全填充后,将newMap的值分配给currentMap 。 请注意,即使其他线程可能正在访问该对象的值,该赋值语句也是一个单元操作,不需要同步。生产者线程可以由于计时器而定期执行,也可以是在某些外部数据(例如数据库)发生更改时唤醒的侦听器。
需要使用currentMap内容的使用者线程只需访问该对象并执行get操作即可。一旦将newMap分配给currentMap ,内容就永远不会改变。 实际上, HashMap是不可变的。 这允许多个get操作并行运行,这可以极大地提高性能。
读取数据时唯一可能更改的是对currentMap变量的对象引用。 生产者可以在使用者线程访问该值的同时,用一个新值覆盖当前值。 因为对象引用是Java语言中的单元操作,所以生产者和使用者在访问该对象时都不需要同步。 可能发生的最坏情况是,消费者获得对currentMap的引用,然后生产者将引用与新内容覆盖在一起。 在这种情况下,使用者线程将使用稍微过时的数据。 如果使用者线程在生产者线程准备好运行之前执行了第二秒,则会发生相同的结果。 通常,这不会引起任何问题。
当确实发生这种竞争时,使用者线程可能会引用数据的“旧”版本。 “新”对象引用已覆盖了旧对象,但是某些消费者仍然有对旧对象的引用。 当最后一个使用者完成对旧对象的引用时,该对象将超出范围并被垃圾回收。 Java运行时会跟踪何时发生。 应用程序不需要显式释放旧对象,因为它会自动发生。
可以根据应用程序的需要定期创建新版本的currentMap 。 通过执行上述步骤,可以确保这些更新安全且重复地进行。
的synchronized块和volatile清单1中的关键字是必需的,因为没有之前发生的写入到之间存在关系currentMap并从读取currentMap 。 结果,如果未使用synchronized块和volatile关键字,则读取线程可能会看到垃圾。 生产者线程将数据结构放在其他线程将在其中读取的位置。 它负责确保使用者线程看到一致的视图。 事实是,数据结构在发布后不会被修改。 在这种情况下(发布有效地不变的对象图),所需要做的就是安全地发布根对象引用。 最简单的方法是使参考volatile 。 您还可以同步对根引用的访问,但这可能是排队点。 我们正在努力避免排队。 Brian Goetz将此方法称为“廉价读写锁”技巧(请参阅参考资料 )。
本文的技术适用于共享数据很少更改且可由多个执行线程同时访问的情况。 它仅适用于不需要绝对最新数据的情况。
最终结果是对共享数据的非同步访问,该访问可能随时间变化。 在需要高性能的环境中,此技术使您可以避免在应用程序内出现不必要的排队点。
重要的是要注意,由于Java内存模型的复杂性,此处描述的技术仅在Java 1.5及更高版本中有效。 在早期的Java版本中,客户端应用程序有查看未完全填充的HashMap风险。
翻译自: https://www.ibm.com/developerworks/java/library/j-hashmap/index.html
相关资源:关于如何解决HashMap线程安全问题的介绍