当且仅当对的引用HashMap
被安全发布时,您的惯用法才是安全的。而不是有关的内部事情HashMap
本身,安全出版物与构建线程如何使得参考地图到其他线程是可见的交易。
基本上,这里唯一可能的竞争是在的构造与在HashMap
完全构造之前可能会访问它的任何读取线程之间。大部分讨论是关于map对象状态发生的事情,但这是无关紧要的,因为您从未修改过它-因此,唯一有趣的部分是如何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]:
- 通过适当锁定的字段交换参考(JLS 17.4.5)
- 使用静态初始化程序进行初始化存储(JLS 12.4)
- 通过volatile字段(JLS 17.4.5)或由于此规则而通过AtomicX类交换引用
- 将值初始化为最终字段(JLS 17.5)。
对于您的情况最有趣的是(2),(3)和(4)。特别是(3)直接适用于我上面的代码:如果将以下声明转换MAP
为:
public static volatile HashMap<Object, Object> MAP;
那么一切都是洁净的:看到非null值的读者必然与该存储与to 发生事前关系,MAP
因此看到与该地图初始化关联的所有存储。
其他方法会更改方法的语义,因为(2)(使用静态初始化器)和(4)(使用final)都暗示您不能MAP
在运行时动态设置。如果您不需要这样做,则只需声明MAP
为a static final HashMap<>
即可确保发布安全。
实际上,这些规则对于安全访问“未经修改的对象”很简单:
如果要发布的对象不是固有不变的(如在声明的所有字段中一样final
),并且:
- 您已经可以创建将在声明a时分配的对象:仅使用一个
final
字段(包括static final
静态成员)。
- 您希望稍后在引用可见之后分配对象:使用volatile字段b。
而已!
实际上,它非常有效。static final
例如,使用字段可以使JVM假定该值在程序生命周期内保持不变,并且可以对其进行大量优化。final
成员字段的使用允许大多数体系结构以与普通字段读取等效的方式读取该字段,并且不会阻止进一步的优化c。
最后,使用volatile
确实会产生一些影响:在许多体系结构(例如x86,特别是那些不允许读取传递读取的结构)上都不需要硬件障碍,但是在编译时可能不会发生一些优化和重新排序-但这效果一般很小。作为交换,您实际上得到的不仅仅是所需的内容-您不仅可以安全地发布一个HashMap
,还可以存储更多未修改的HashMap
s,以使用相同的参考文献,并确保所有读者都能看到安全发布的地图。
有关更多详细信息,请参阅Shipilev或Manson和Goetz的本常见问题解答。
[1]直接引用shipilev。
一个这听起来很复杂,但我的意思是,你可以在指定施工时间参考-无论是在申报点或在构造函数(成员字段)或静态初始化(静态字段)。
b(可选)您可以使用一种synchronized
方法来获取/设置或一个AtomicReference
或某物,但是我们正在谈论的是您可以做的最少工作。
c某些内存模型非常弱的体系结构(我正在看着您,Alpha)在读取之前可能需要某种类型的读取屏障final
-但是今天这些很少见。