当且仅当对 的引用被安全发布时,您的成语才是安全的。安全发布处理的不是与自身内部相关的任何内容,而是构造线程如何使对映射的引用对其他线程可见。HashMap
HashMap
基本上,这里唯一可能的竞争是在完全构建之前可能访问它的任何读取线程的构造之间。大部分讨论都是关于map对象的状态发生了什么,但这无关紧要,因为你永远不会修改它 - 所以唯一有趣的部分是如何发布引用的。HashMap
HashMap
例如,假设您发布地图时如下所示:
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
- 通过正确锁定的字段交换引用 (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;
那么一切都是犹太洁食的:看到非空值的读者必然与商店有发生之前的关系,因此可以看到与地图初始化相关的所有商店。MAP
其他方法会更改方法的语义,因为 (2)(使用静态初始化器)和 (4)(使用 final)都意味着您无法在运行时动态设置。如果您不需要这样做,那么只需声明为a,即可保证安全发布。MAP
MAP
static final HashMap<>
在实践中,这些规则对于安全访问“永不修改的对象”很简单:
如果您要发布的对象本身不是不可变的(就像在所有声明的字段中一样),并且:final
- 您已经可以创建将在声明a 时分配的对象:只需使用字段(包括静态成员)。
final
static final
- 您希望稍后在引用已经可见之后分配对象:使用易失性字段b。
就是这样!
在实践中,它非常有效。例如,使用字段允许 JVM 假定该值在程序的生命周期内保持不变,并对其进行大量优化。成员字段的使用允许大多数体系结构以等效于普通字段读取的方式读取字段,并且不会阻止进一步的优化c。static final
final
最后,使用确实产生了一些影响:许多架构(例如x86,特别是那些不允许读取传递读取的体系结构)不需要硬件屏障,但是在编译时可能不会发生一些优化和重新排序 - 但这种影响通常很小。作为交换,您实际上获得的比您要求的更多 - 您不仅可以安全地发布一个,还可以存储尽可能多的未修改的参考,并确信所有读者都将看到安全发布的地图。volatile
HashMap
HashMap
有关更多血腥的细节,请参阅Shipilev或Manson和Goetz的常见问题解答。
[1] 直接引用shipilev。
a 这听起来很复杂,但我的意思是,您可以在构造时分配引用 - 无论是在声明点还是在构造函数(成员字段)或静态初始值设定项(静态字段)中。
b (可选)您可以使用方法来获取/设置,或者一个或什么,但我们谈论的是您可以执行的最小工作。synchronized
AtomicReference
c 一些内存模型非常弱的架构(我正在看你,Alpha)可能需要某种类型的读取屏障才能读取 - 但这些在今天非常罕见。final