从多个线程获取java.util.HashMap中的值是否安全(无需修改)?

问题

有一种情况会构建一个 Map ,一旦它被初始化,它将永远不会被再次修改。但是,它将从多个线程访问(仅通过get(key))。以这种方式使用ajava.util.HashMap是否安全?

(目前,我很高兴使用ajava.util.concurrent.ConcurrentHashMap,没有测量需要提高性能,但是如果简单的HashMap就足够好了。因此,这个问题不是"我应该使用哪一个?"也不是性能问题。问题是"它会安全吗?")


#1 热门回答(70 赞)

杰里米·曼森(Jeremy Manson),就Java内存模型而言,有一个关于这个主题的三部分博客 - 因为从本质上讲,你提出的问题是"访问不可变的HashMap是否安全" - 答案是肯定的。但是你必须回答那个问题的谓词 - "我的HashMap是不可变的"。答案可能会让你感到惊讶 - Java有一套相对复杂的规则来确定不变性。

有关该主题的更多信息,请阅读Jeremy的博客文章:

关于Java中不变性的第1部分:http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html

关于Java中不变性的第2部分:http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

关于Java中不变性的第3部分:http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html


#2 热门回答(34 赞)

从同步的角度来看,读取是安全的,但不是内存的立场。这在Java开发人员中被广泛误解,包括Stackoverflow。 (观察this answer的评级。)

如果其他线程正在运行,如果没有内存写出当前线程,它们可能看不到HashMap的更新副本。内存写入通过使用synchronized或volatile关键字或通过使用某些java并发结构来实现。

有关详细信息,请参见Brian Goetz's article on the new Java Memory Model


#3 热门回答(27 赞)

当且仅当**引用了HashMap发布时,你的习语才是安全的5760003546。安全发布方式与构造线程如何使对 Map 的引用对其他线程可见,而不是与内部相关的任何内容。

基本上,这里唯一可能的竞争是在构造HashMap和任何可以在完全构造之前访问它的读取线程之间。大多数讨论是关于 Map 对象的状态发生了什么,但这是无关紧要的,因为你永远不会修改它 - 所以唯一有趣的部分是如何发布HashMap参考。

例如,假设你像这样发布 Map :

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

...并且在某个时刻setMap()使用 Map 调用,其他线程正在使用SomeClass.MAP来访问 Map ,并检查null如下:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

这是不安全即使它可能看起来好像是。问题是在SomeObject.MAP的集合与另一个线程上的后续读取之间存在nohappens-before关系,因此读取线程可以自由地看到部分构造的映射。这几乎可以做任何事情,甚至在实践中也可以做像put the reading thread into an infinite loop这样的事情。

为了安全地发布 Map ,你需要在引用到HashMap(即出版物)的参考文献与该参考文献的后续阅读者(即消费)之间建立关系。方便的是,只有一些容易记住的方法[225] [1]:

  • 通过正确锁定的字段交换引用(JLS 17.4.5)
  • 使用静态初始化程序进行初始化存储(JLS 12.4)
  • 通过易失性字段(JLS 17.4.5)交换引用,或者作为此规则的结果,通过AtomicX类交换引用
  • 将值初始化为最终字段(JLS 17.5)。

对你的场景最感兴趣的是(2),(3)和(4)。特别是,(3)直接适用于我上面的代码:如果你将声明MAP转换为:

public static volatile HashMap<Object, Object> MAP;

然后一切都是犹太人:看到anon-nullvalue的读者必须在与商店之前的关系中达到-MAP,因此看到与 Map 初始化相关的所有商店。

其他方法改变了方法的语义,因为(2)(使用静态初始化)和(4)(使用最终)意味着你不能在运行时动态设置MAP。如果你不需要这样做,那么只需声明我们保证安全发布即可获得astatic final HashMap<>

在实践中,规则对于安全访问"从未修改过的对象"很简单:

如果要发布一个非本机不可变的对象(如在所有字段中声明为final),并且:

  • 你已经可以创建将在声明时分配的对象:只使用最终字段(包括静态成员的静态最终字段)。
  • 你希望稍后在引用可见之后分配对象:使用volatile fieldb。

而已!

在实践中,它非常有效。例如,使用astatic final字段允许JVM假定该值在程序的生命周期内保持不变并对其进行大量优化。 afinalmember字段的使用允许大多数体系结构以与正常字段读取等效的方式读取字段,并且不会抑制进一步的优化。

最后,使用volatile会产生一些影响:在许多体系结构上都不需要硬件屏障(例如x86,特别是那些不允许读取传递读取的体系结构),但是某些优化和重新排序可能不会在编译时发生 - 但这种影响一般都很小。作为交换,你实际上获得的不仅仅是你所要求的 - 你不仅可以安全地发布oneHashMap,你可以存储更多未修改的HashMap,因为你想要相同的参考,并确保所有读者都能看到安全发布的 Map 。

有关更多详细信息,请参阅Shipilevthis FAQ by Manson and Goetz

[1]直接引用自shipilev

a听起来很复杂,但我的意思是你可以在构造时分配引用 - 在声明点或构造函数(成员字段)或静态初始化器(静态字段)。

b或者,你可以使用asynchronized方法来获取/设置,或者使用某些东西,但我们正在谈论你可以做的最低限度的工作。

c一些具有非常弱内存模型的架构(我看着你,Alpha)可能需要在afinal读取之前使用某种类型的读屏障 - 但这些在今天非常罕见。