从多个线程(不做任何修改)从java.util.HashMap获取值是否安全?

时间:2020-03-06 14:27:09  来源:igfitidea点击:

在某些情况下,将构造一个映射,并且一旦对其进行初始化,就永远不会再对其进行修改。但是,它将从多个线程访问(仅通过get(key))。这样使用java.util.HashMap是否安全?

(目前,我很高兴使用java.util.concurrent.ConcurrentHashMap,并且没有提高性能的必要性,但只是想知道简单的HashMap是否足够。因此,这个问题不是"哪个我应该使用吗?"也不是性能问题。而是"是否安全?")

解决方案

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

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

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

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

再看一看后,我在java doc中找到了这个(重点是我的):

Note that this implementation is not
  synchronized.  If multiple threads
  access a hash map concurrently, and at
  least one of the threads modifies the
  map structurally, it must be
  synchronized externally. (A structural
  modification is any operation that
  adds or deletes one or more mappings;
  merely changing the value associated
  with a key that an instance already
  contains is not a structural
  modification.)

这似乎暗示着它是安全的,假设存在相反的说法。

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

因此,在不太可能的情况下,现有代码可能会在设置过程中向集合添加null(可能是在某种情况下发生故障),按所述方式替换集合将改变功能行为。

就是说,只要我们不执行其他任何操作,从HashMap进行的并发读取都是安全的。

[编辑:通过"并发读取",我的意思是没有并发修改。

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

我的目的不是与有关Java内存模型的更详细的答案相矛盾。该答案旨在指出,除了并发问题之外,ConcurrentHashMap和HashMap之间至少还有一个API差异,这甚至可能破坏单线程程序,而该程序将另一个程序替换为另一个程序。]

涉及Java内存模型的神杰里米·曼森(Jeremy Manson)在这个主题上有一个由三部分组成的博客,因为从本质上讲,我们在问"是否可以安全地访问不变的HashMap"这一问题的答案是肯定的。但是我们必须回答这个谓词"我的HashMap是不可变的"。答案可能会让我们感到惊讶,Java拥有一套相对复杂的确定不变性的规则。

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

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

第2部分关于Java的不变性:

http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

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

从同步的角度看,读取是安全的,但从内存的角度来看,读取不是安全的。在Java开发人员中,包括在Stackoverflow上,这都是人们普遍误解的东西。 (观察该答案的等级以作证明。)

如果我们正在运行其他线程,如果当前线程没有写出内存,则他们可能看不到HashMap的更新副本。通过使用synced或者volatile关键字,或者通过使用某些Java并发构造,可以进行内存写操作。

有关详细信息,请参见Brian Goetz在新的Java内存模型上的文章。

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

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

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

因此,我们描述的场景是需要将大量数据放入Map中,然后在完成填充后将其视为不可变的。一种"安全"的方法(意味着我们要强制将其确实视为不可变的)是在准备使其成为不可变的时,将引用替换为Collections.unmodifiableMap(originalMap)。

段落数量不匹配