从multithreading获取java.util.HashMap的值是否安全(不需要修改)?
有一种情况是地图将被构build,一旦它被初始化,它将不会再被修改。 然而,它将被访问(仅通过get(key))来自多个线程。 以这种方式使用java.util.HashMap
是否安全?
(目前,我很高兴地使用java.util.concurrent.ConcurrentHashMap
,并没有任何需要提高性能的措施,但是只要简单的HashMap
就足够了就可以了,所以这个问题不是 “我该用哪一个? “这也不是一个performance问题,而是问题是”安全吗?“)
当且仅当 HashMap
的引用被安全地发布时,你的成语才是安全的。 安全的发布不是关于HashMap
本身内部的任何事情,而是处理构造线程如何使对地图的引用对其他线程可见。
基本上,这里唯一可能的竞争是在HashMap
的构造和任何在完全构造之前可以访问的读线程之间。 大部分的讨论是关于地图对象的状态会发生什么,但这是不相关的,因为你永远不会修改它 – 所以唯一有趣的部分是如何发布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
(即发布 )的写入与随后的参考读者(即,消费)之间build立一个“先发生”的关系。 方便的是,只有几个易于记忆的方法来实现这一点[1] :
- 通过正确locking的字段交换参考( 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)(使用static initalizer)和(4)(使用final )意味着你不能在运行时dynamic地设置MAP
。 如果你不需要这样做,那么只需将MAP
声明为一个static final HashMap<>
,就可以保证安全发布。
在实践中,规则很简单,可以安全地访问“从未修改的对象”:
如果你正在发布一个不是固有的不可变的对象(如所有声明为final
)和:
- 您已经可以创build将在声明a时分配的对象:只需使用
final
字段(包括静态成员的static final
)。 - 您希望在引用已经可见之后稍后分配对象:使用易失性字段b 。
而已!
在实践中,这是非常有效的。 例如,使用static final
字段允许JVM在程序生命周期中假定该值不变,并对其进行优化。 final
成员字段的使用允许大多数体系结构以等同于正常字段读取的方式读取字段,并且不禁止进一步的优化。
最后, volatile
的使用确实有一些影响:在许多体系结构(如x86,尤其是那些不允许读取通过读取的体系结构)上不需要硬件屏障,但是在编译时可能不会发生一些优化和重新sorting – 但这个效果一般很小。 作为交换,实际上你得到的不仅仅是你所要求的 – 你不仅可以安全地发布一个HashMap
,你可以存储更多的未修改的HashMap
只要你想有相同的参考,并确保所有的读者都能安全地看到发表的地图。
有关更多血淋淋的细节,请参阅Shipilev或Manson和Goetz的常见问题解答 。
[1]直接从shipilev引用。
这听起来很复杂,但我的意思是,你可以在构造时分配引用 – 在声明点或构造函数(成员字段)或静态初始化程序(静态字段)。
b或者,你可以使用一个synchronized
方法来获取/设置,或者一个AtomicReference
或者其他东西,但是我们正在谈论你可以做的最低限度的工作。
c一些内存模型非常弱的体系结构(我在看你 ,Alpha)在final
读取之前可能需要某种types的读取屏障 – 但是这些在今天非常less见。
关于Java内存模型的神 – Jeremy Manson有一个关于这个主题的三部分博客 – 因为从本质上讲,你提出的问题是“访问一个不可变的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
读取从同步的angular度来看是安全的,但不是从内存的angular度来看的。 这在Java开发人员(包括Stackoverflow)中被广泛误解。 (请注意这个答案的等级来certificate。)
如果有其他线程在运行,如果没有写入当前线程的内存,它们可能不会看到HashMap的更新副本。 内存写入通过使用synchronized或volatile关键字,或通过使用一些java并发结构来实现。
有关详细信息,请参阅Brian Goetz关于新的Java内存模型的文章 。
稍微看了一下后,我在java doc (重点是我的)中发现了这个:
请注意,此实现不同步。 如果多个线程同时访问哈希映射,并且至less有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;只是更改与实例已包含的关键字相关联的值不是结构修改。)
这似乎意味着它是安全的,假设声明的反面是真实的。
尽pipe有一个重要的转折点。 访问映射是安全的,但通常不能保证所有线程都能看到完全相同的HashMap状态(以及值)。 这可能发生在多处理器系统上,一个线程(例如,填充它的那个线程)完成的对HashMap的修改可以位于该CPU的高速caching中,并且不会被运行在其他CPU上的线程看到,直到内存围栏操作确保caching一致性。 Java语言规范是明确的:解决scheme是获取一个锁(synchronized(…)),它发出一个内存篱笆操作。 所以,如果你确定在填充HashMap之后每个线程都获得了ANY锁,那么就可以从任何线程访问HashMap,直到HashMap被再次修改为止。
需要注意的是,在某些情况下,来自非同步HashMap的get()会导致无限循环。 如果并发的put()导致Map的重新散布,就会发生这种情况。
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
在新的内存模型下,类似于构造函数中最后一个字段的写入和另一个线程中该对象的共享引用的初始加载之间的一个“先发生”关系。 …
所以你所描述的场景是你需要把一堆数据放到一个Map中,然后当你完成填充时你把它当作不可变的。 一种“安全”的方法(意思是强制将它视为不可变的)是当你准备使它不可变时,用Collections.unmodifiableMap(originalMap)replace引用。
例如,如果同时使用地图可能会失败,以及我提到的build议解决方法,请查看这个bug游行条目: bug_id = 6423457
要警告的是,即使在单线程代码中,用HashMapreplaceConcurrentHashMap也可能不安全。 ConcurrentHashMap禁止将null作为键或值。 HashMap不禁止它们(不要问)。
因此,在不太可能的情况下,您现有的代码可能会在安装期间(可能是某种失败情况)向集合中添加空值,如上所述replace集合将会改变function行为。
也就是说,只要你不做任何事情,从HashMap的并发读取是安全的。
[编辑:通过“并发读取”,我的意思是,也没有同时修改。
其他答案解释如何确保这一点。 一种方法是使地图不可变,但没有必要。 例如,JSR133内存模型显式定义将线程启动为一个同步动作,这意味着线程A在启动线程B之前所做的更改在线程B中可见。
我的意图不是要与有关Java内存模型的更详细的答案相抵触。 这个答案的目的是指出,即使除了并发问题,ConcurrentHashMap和HashMap之间至less有一个API区别,即使是一个单一的线程程序,它们也可能会取代另一个。
http://www.docjar.com/html/api/java/util/HashMap.java.html
这里是HashMap的来源。 如你所知,这里绝对没有locking/互斥锁代码。
这意味着虽然在multithreading情况下可以从HashMap中读取,但是如果有多个写入的话,我肯定会使用ConcurrentHashMap。
有趣的是,.NET HashTable和Dictionary <K,V>都内置了同步代码。
如果初始化和每个put是同步的,则保存。
下面的代码是保存的,因为类加载器将负责同步:
public static final HashMap<String, String> map = new HashMap<>(); static { map.put("A","A"); }
下面的代码被保存,因为volatile的写入将处理同步。
class Foo { volatile HashMap<String, String> map; public void init() { final HashMap<String, String> tmp = new HashMap<>(); tmp.put("A","A"); // writing to volatile has to be after the modification of the map this.map = tmp; } }
如果成员variables是final的,这也将起作用,因为final也是不稳定的。 如果该方法是一个构造函数。