从 HashMap 检索时的 NPE,即使 containsKey() 在多线程环境中返回 true

Sun*_*ini 3 multithreading synchronization kotlin

我们正在尝试存储特定键的唯一对象。当在多线程环境中调用 getMyObject 时,我们在 return 语句时收到 null ptr 异常

object SampleClass
{
 
    fun getMyObject(Id : String) : MyObject
    {
        if(!myMap.containsKey(Id))
        {
            synchronized(SampleClass)
            {
                if(!myMap.containsKey(Id))
                {
                    myMap[Id] = MyObject()
                }
            }
        }
        return myMap[Id]!!
    }

    private val myMap = HashMap<String,MyObject>()
}
Run Code Online (Sandbox Code Playgroud)

看起来即使 contains 方法返回 true,当我们尝试获取该值时,该值仍返回 null。我不确定其背后的原因是什么。

Moa*_*ser 5

如果你试图超越内存模型,就会遇到这种麻烦。如果你查看 的HashMap源代码,你会发现containsKey实现如下:

public boolean containsKey(Object key) {
  return getNode(key) != null;
}
Run Code Online (Sandbox Code Playgroud)

请注意,仅当存在HashMap.Node与给定键对应的对象时,它才返回 true。现在,这是如何get实现的:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(key)) == null ? null : e.value;
}
Run Code Online (Sandbox Code Playgroud)

您所看到的是不安全发布问题的一个实例。假设 2 个线程(A 和 B)调用getMyObject一个不存在的密钥。synchronizedA 稍微领先于 B,因此它在 B 调用 之前进入块containsKey。特别是,Aput在 B 之前调用containsKey。调用put创建一个新Node对象并将其放入哈希映射的内部数据结构中。

containsKey现在,考虑 B在 A 存在该块之前调用的情况synchronized。B可能会看到NodeA 放置的对象,在这种情况下containsKey返回 true。但此时该节点是不安全发布的,因为它被B以非同步方式并发访问。不能保证它的构造函数(设置其value字段的构造函数)已被调用。即使它被调用,也不能保证value引用(或构造函数设置的任何引用)与节点引用一起发布。这意味着 B 可以看到不完整的节点:节点引用,但看不到其值或其任何字段。当 B 进行到 时get,它读取null为不安全发布节点的值。因此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)
Run Code Online (Sandbox Code Playgroud)

以上只是可能出现问题的一种情况(请参阅评论)。为了避免这种危险,要么使用ConcurrentHashMap(例如),要么永远不要在块之外同时map.computeIfAbsent(key, key -> new MyObject())访问您的。HashMapsynchronized