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

2022-08-31 07:13:48

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

(目前,我很高兴使用 ,并且没有提高性能的衡量需求,但我只是好奇一个简单的是否足够。因此,这个问题不是“我应该使用哪一个?”也不是一个性能问题。相反,问题是“它安全吗?java.util.concurrent.ConcurrentHashMapHashMap


答案 1

Jeremy Manson,Java Memory Model的上帝,有一个关于这个主题的三部分博客 - 因为本质上你是在问“访问一个不可变的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


答案 2

当且仅当对 的引用被安全发布时,您的成语才是安全的安全发布处理的不是与自身内部相关的任何内容,而是构造线程如何使对映射的引用对其他线程可见。HashMapHashMap

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

例如,假设您发布地图时如下所示:

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

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

...并且在某些时候使用映射调用,并且其他线程正在使用来访问映射,并检查 null,如下所示:setMap()SomeClass.MAP

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

这是不安全的,即使它可能看起来好像是安全的。问题在于,在另一个线程上的集合和后续读取之间没有发生之前的关系,因此读取线程可以自由地看到部分构造的映射。这几乎可以做任何事情,甚至在实践中它也会做一些事情,比如将读取线程放入无限循环中。SomeObject.MAP

为了安全地发布地图,您需要在对参考文献(即出版物的编写与该参考文献的后续读者(即消费)之间建立一种发生之前的关系。方便的是,只有几种易于记忆的方法可以实现这一目标[1]HashMap

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

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

public static volatile HashMap<Object, Object> MAP;

那么一切都是犹太洁食的:看到非空值的读者必然与商店有发生之前的关系,因此可以看到与地图初始化相关的所有商店。MAP

其他方法会更改方法的语义,因为 (2)(使用静态初始化器)和 (4)(使用 final)都意味着您无法在运行时动态设置。如果您不需要这样做,那么只需声明为a,即可保证安全发布。MAPMAPstatic final HashMap<>

在实践中,这些规则对于安全访问“永不修改的对象”很简单:

如果您要发布的对象本身不是不可变的(就像在所有声明的字段中一样),并且:final

  • 您已经可以创建将在声明a 时分配的对象:只需使用字段(包括静态成员)。finalstatic final
  • 您希望稍后在引用已经可见之后分配对象:使用易失性字段b

就是这样!

在实践中,它非常有效。例如,使用字段允许 JVM 假定该值在程序的生命周期内保持不变,并对其进行大量优化。成员字段的使用允许大多数体系结构以等效于普通字段读取的方式读取字段,并且不会阻止进一步的优化cstatic finalfinal

最后,使用确实产生了一些影响:许多架构(例如x86,特别是那些不允许读取传递读取的体系结构)不需要硬件屏障,但是在编译时可能不会发生一些优化和重新排序 - 但这种影响通常很小。作为交换,您实际上获得的比您要求的更多 - 您不仅可以安全地发布一个,还可以存储尽可能多的未修改的参考,并确信所有读者都将看到安全发布的地图。volatileHashMapHashMap

有关更多血腥的细节,请参阅ShipilevManson和Goetz的常见问题解答


[1] 直接引用shipilev


a 这听起来很复杂,但我的意思是,您可以在构造时分配引用 - 无论是在声明点还是在构造函数(成员字段)或静态初始值设定项(静态字段)中。

b (可选)您可以使用方法来获取/设置,或者一个或什么,但我们谈论的是您可以执行的最小工作。synchronizedAtomicReference

c 一些内存模型非常弱的架构(我正在看,Alpha)可能需要某种类型的读取屏障才能读取 - 但这些在今天非常罕见。final


推荐