有一种情况会构建一个地图,一旦它被初始化,它将永远不会被再次修改.但是,它将从多个线程访问(仅通过get(key)).java.util.HashMap
以这种方式使用是否安全?
(目前,我很高兴使用a java.util.concurrent.ConcurrentHashMap
,并没有明确的需要提高性能,但我只是好奇,如果一个简单HashMap
就足够了.因此,这个问题不是 "我应该使用哪一个?"也不是性能问题.相反,问题是"它会安全吗?")
杰里米·曼森(Jeremy Manson),就Java内存模型而言,有一个关于这个主题的三部分博客 - 因为从本质上讲,你问的问题是"访问不可变的HashMap是否安全" - 答案是肯定的.但是你必须回答那个问题的谓词 - "我的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
当且仅当HashMap
对此的引用安全发布时,您的习语才是安全的.安全发布不是与内部HashMap
本身相关的任何内容,而是涉及构造线程如何使对地图的引用对其他线程可见.
基本上,这里唯一可能的竞争是在HashMap
完全构造之前可以访问它的任何读取线程的构造之间.大多数讨论是关于地图对象的状态发生了什么,但这是无关紧要的,因为你永远不会修改它 - 所以唯一有趣的部分是如何HashMap
发布引用.
例如,假设您像这样发布地图:
class SomeClass { public static HashMap
...在某些时候setMap()
用map调用,其他线程SomeClass.MAP
用来访问map,并像这样检查null:
HashMapmap = SomeClass.MAP; if (map != null) { .. use the map } else { .. some default behavior }
这虽然看起来好像是安全的,但并不安全.问题是在另一个线程上的集合和后续读取之间没有发生之前的关系SomeObject.MAP
,因此读取线程可以自由地看到部分构造的映射.这几乎可以做任何事情,甚至在实践中它也可以将读取线程放入无限循环中.
为了安全地发布地图,您需要建立之前发生的关系的参考书面的HashMap
(即出版)和引用(即消费)的后续读者.方便的是,只有几个简单的记忆的方式来做到这[1] :
通过正确锁定的字段交换引用(JLS 17.4.5)
使用静态初始化程序来执行初始化存储(JLS 12.4)
通过易失性字段(JLS 17.4.5)或作为此规则的结果,通过AtomicX类交换引用
将值初始化为最终字段(JLS 17.5).
对你的场景最感兴趣的是(2),(3)和(4).特别是,(3)直接适用于我上面的代码:如果你将声明转换MAP
为:
public static volatile HashMapMAP;
然后一切都是犹太人:看到非空值的读者必须与商店之前发生过关系,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.
a听起来很复杂,但我的意思是你可以在构造时分配引用 - 在声明点或构造函数(成员字段)或静态初始化器(静态字段).
b(可选)您可以使用synchronized
方法来获取/设置,或者使用某种方法AtomicReference
,但我们正在讨论您可以执行的最低限度的工作.
c一些具有非常弱内存模型的架构(我看着你,Alpha)在阅读之前可能需要某种类型的读屏障final
- 但这些在今天非常罕见.
从同步的角度来看,读取是安全的,但不是内存的立场.这在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设置为最终字段,并在构造函数完成后将其安全发布。
在新的内存模型下,类似于构造函数中最终字段的写入与另一个线程中对该对象的共享引用的初始加载之间的事前发生关系。...