如果你试图智取内存模型,就会遇到这种麻烦.如果查看HashMap
的源代码,您会发现containsKey
实现为:
public boolean containsKey(Object key) {
return getNode(key) != null;
}
请注意,只有当给定键对应HashMap.Node
个对象时,它才会返回true.现在,get
是如何实现的:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
你看到的是unsafe publication问题的一个例子.假设2个线程(A和B)调用getMyObject
以获取不存在的密钥.A略高于B,所以在B调用containsKey
之前,它进入synchronized
块.特别地,A在B呼叫containsKey
之前呼叫put
.对put
的调用创建了一个新的Node
对象,并将其放入哈希映射的内部数据 struct 中.
现在,考虑B在A存在synchronized
块之前调用containsKey
的情况.B might查看A放置的Node
对象,在这种情况下,containsKey
返回true.然而,此时 node 被安全地发布,因为它是由B以非同步方式并发访问的.无法保证其构造函数(设置其value
字段的构造函数)已被调用.即使调用了它,也不能保证value
引用(或构造函数设置的任何引用)与 node 引用一起发布.这意味着B可以看到一个不完整的 node : node 引用,但不能看到其值或任何字段.当B前进到get
时,它读取null
作为未安全发布的 node 的值.因此,NullPointerException
.
下面是一个特殊的图表,用于可视化:
Thread A Thread B
- Enter the synchronized block
- Call hashMap.put(...)
- Insert a new Node
- See the newly inserted (but not yet
initialized from the perspective of B)
Node in HashMap.containsKey
- Return node.value (still null)
from HashMap.get
- !! throws a `NullPointerException`
...
- Exit the synchronized block
(now the node is safely published)
以上只是一个可能出错的场景(见注释).为避免此类危险,请使用ConcurrentHashMap
(例如map.computeIfAbsent(key, key -> new MyObject())
)或不要在synchronized
街区外同时访问HashMap
.