有一种情况下,地图将构成,而一旦它被初始化,它将永远不会被修改了。它将然而,被访问的(获得通过(key)only)从多线程。它是安全的使用 java.util.HashMap 在这一方式?

(目前,我愉快地使用 java.util.concurrent.ConcurrentHashMap, ,以及有没有测量需要提高性能,但我只是好奇如果一个简单的 HashMap 就足够了。因此,这个问题是 "我应该使用哪一个?"也不是这一性能的问题。相反,问题是"它是安全的吗?")

有帮助吗?

解决方案

你的惯用语是安全的 如果且只有如果 参考 HashMap安全地公布.而不是什么有关的内部 HashMap 本身, 安全出版物 涉及如何构建线使得参考地图上可见于其他线。

基本上,只有可能在这里种族之间的建设 HashMap 和任何阅读线,可以访问它之前,它是完全建成。大部分的讨论是关于什么状态地图的对象,但这是无关紧要因为你永远不会修改它-所以只有趣的部分是如何的 HashMap 参考出版。

例如,想象一下,你发布的地图像这样:

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

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

...并且在某些点 setMap() 被称为地图和其他线使用 SomeClass.MAP 访问的地图,检查null这样的:

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

这个是 不安全 虽然它可能似乎是。问题是,有没有 发生之前 之间的关系设置的 SomeObject.MAP 和随后的阅读另外一个线程,因此阅读线是免费的,见到一个部分构成的图。这几乎可以做 任何东西 甚至在实践中它不喜欢的东西 把读线到无限循环.

安全发布的地图,需要建立一个 发生之前 之间的关系 编写参考 来的 HashMap (即, 出版物)和随后的读者参考(即消耗量)。方便地,只有少数易于记忆的方法 完成[1]:

  1. 交换的参照,通过一个正确锁定的领域(捷尔思17.4.5)
  2. 使用静态的初始化做初始存储(捷尔思12.4)
  3. 汇参考通过挥发性的领域(捷尔思17.4.5),或作为后果这项规则,通过AtomicX类
  4. 初始化价值进入最后的领域(捷尔思17.5).

最有趣你的方案(2)、(3)和(4)条。特别是,(3)直接适用的代码,我已经上述:如果你改变的声明 MAP 为:

public static volatile HashMap<Object, Object> MAP;

然后一切都是犹太:读者看到一个 非空 价值一定有一个 发生之前 关店 MAP 因此看到所有的存储相关的地图的初始化。

其他方法变化的语义学的方法,因为这两个(2)(使用的静态initalizer)和(4)(使用 最终)意味着不能设置 MAP 动态在运行时间。如果你不 需要 要做到这一点,那么刚刚宣布 MAP 作为一个 static final HashMap<> 和你们保证安全的出版物。

在实践中,规则是简单的用于安全的访问"永远不会-经过修改的对象":

如果你发布一个对象不是 固有的不可改变的 (如在所有领域的宣布 final)并且:

  • 你已经可以创建的目的,将被分配的时刻的宣言一个:只是使用一个 final 领域(包括 static final 为静态的成员)。
  • 你想要分配的对象后,在参考已经可见:使用挥发性的领域b.

这就是它!

在实践中,这是非常有效。使用 static final 领域,例如,允许JVM承担的价值是不变的生活方案和优化它严重。使用 final 成员领域的允许 大多数 结构阅读领域中的方式相当于正常现场读和不抑制进一步的优化c.

最后,使用 volatile 不会有一些影响:没有硬件的障碍是需要在许多的体系结构(例如x86,特别是那些不允许读通过读取),但有一些优化和重新排序,可能不会发生在汇编时间,但这种影响通常很小。在交流中,你实际上获得更多比你的要求不仅可以安全地公布一个 HashMap, 你可以存储更多的不修饰 HashMaps你想要的,以相同的参考,并确保所有的读者将会看到一个安全地出版的地图。

更多细节,请参阅 Shipilev这个常见问题通过森和Goetz.


[1]直接引用 shipilev.


一个 这听起来复杂,但我的意思是,你可以指定的参考在施工时间或者在该宣言点或在构造(成员字段)或静态的初始化(静态场)。

b 或者您可以使用 synchronized 法get/set,或 AtomicReference 或什么,但我们谈论的是最低的工作可以做。

c一些体系结构中非常弱的存储器模型(我正在看 你的, 阿尔法)可能需要某些类型的阅读障碍之一 final 读但是这些都是非常罕见的今天。

其他提示

杰克米·曼森,关于Java内存模型的上帝,有一个关于这个主题的三部分博客 - 因为实质上你问的是问题<!>;访问不可变的HashMap是否安全<!> QUOT; - 答案是肯定的。但是你必须回答那个问题的谓词 - <!>“;我的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

从同步的角度来看,读取是安全的,但不是内存的立场。这在Java开发人员中被广泛误解,包括Stackoverflow。 (观察此答案的评分以获取证据。)

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

请参阅 Brian Goetz关于新Java内存模型的文章详情。

再看一下之后,我在 java doc (强调我的):

  

请注意,此实现不是   同步。 如果是多个线程   同时访问哈希映射,并在   至少有一个线程修改了   在结构上映射,它必须是   外部同步。(结构性的   修改是任何操作   添加或删除一个或多个映射;   只是改变相关的价值   用一个实例已经的密钥   包含不是结构   修改。)

这似乎暗示它是安全的,假设声明的相反是真的。

需要注意的是,在某些情况下,来自未同步的HashMap的get()会导致无限循环。如果并发put()导致重新映射Map,则会发生这种情况。

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

虽然有一个重要的转折点。访问映射是安全的,但通常不能保证所有线程都能看到HashMap的完全相同的状态(以及因此值)。这可能发生在多处理器系统上,其中由一个线程(例如,填充它的那个)完成对HashMap的修改可以位于该CPU的缓存中,并且在其他CPU上运行的线程不会看到,直到内存栅栏操作为止。执行确保缓存一致性。 Java语言规范在这一方面是明确的:解决方案是获取一个锁(synchronized(...)),它发出一个内存栅栏操作。所以,如果你确定在填充HashMap之后每个线程都获得了任何锁,那么从那时起就可以从任何线程访问HashMap,直到再次修改HashMap为止。

根据 http://www.ibm.com/developerworks/java / library / j-jtp03304 / #初始化安全性你可以使你的HashMap成为一个最后的字段,在构造函数完成后它将被安全地发布。

... 在新的内存模型下,有一些类似于在构造函数中写入最终字段与在另一个线程中对该对象的共享引用的初始加载之间发生之前的关系。 ...

所以你描述的场景是你需要将一堆数据放入Map中,然后当你完成填充它时,你将它视为不可变的。一种方法是<!>“safe <!>”; (意味着你强制执行它确实被视为不可变)是在你准备好使它变为不可变时用Collections.unmodifiableMap(originalMap)替换引用。

有关如果同时使用地图可能会失败的示例以及我提到的建议解决方法的示例,请查看此错误游行条目: bug_id = 6423457

请注意,即使在单线程代码中,用HashMap替换ConcurrentHashMap也可能不安全。 ConcurrentHashMap禁止将null作为键或值。 HashMap不禁止它们(不要问)。

因此,在不太可能的情况下,您的现有代码可能会在设置期间向集合添加null(可能是在某种情况下的故障情况下),如上所述替换集合将改变功能行为。

那就是说,如果你什么也不做,HashMap的并发读取是安全的。

[编辑:通过<!>“并发读取<!>”;,我的意思是也没有并发修改。

其他答案解释了如何确保这一点。一种方法是使地图不可变,但这不是必需的。例如,JSR133内存模型显式定义了将一个线程作为一个同步动作,这意味着线程A在它启动线程B之前所做的更改在线程B中是可见的。

我的意图是不要与Java内存模型的那些更详细的答案相矛盾。这个答案旨在指出,即使除了并发问题之外,ConcurrentHashMap和HashMap之间至少存在一个API差异,这甚至会破坏一个用另一个替换的单线程程序。]

http://www.docjar.com/html /api/java/util/HashMap.java.html

这是HashMap的源代码。正如您所知,那里绝对没有锁定/互斥代码。

这意味着虽然可以在多线程情况下从HashMap读取,但如果有多次写入,我肯定会使用ConcurrentHashMap。

有趣的是.NET HashTable和Dictionary <!> lt; K,V <!> gt;内置了同步代码。

如果初始化和每个put同步,则保存。

以下代码是save,因为类加载器将负责同步:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

以下代码是save,因为写入volatile会处理同步。

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

如果成员变量是final,这也会有效,因为final也是volatile。如果方法是构造函数。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top