ConcurrentHashMap 重新排序指令?

2022-09-03 15:26:36

我正在研究 ConcurrentHashMap 的实现,有一件事让我感到困惑。

/* Specialized implementations of map methods */

        V get(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry<K,V> e = getFirst(hash);
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key)) {
                        V v = e.value;
                        if (v != null)
                            return v;
                        return readValueUnderLock(e); // recheck
                    }
                    e = e.next;
                }
            }
            return null;
        }

    /**
     * Reads value field of an entry under lock. Called if value
     * field ever appears to be null. This is possible only if a
     * compiler happens to reorder a HashEntry initialization with
     * its table assignment, which is legal under memory model
     * but is not known to ever occur.
     */
    V readValueUnderLock(HashEntry<K,V> e) {
        lock();
        try {
            return e.value;
        } finally {
            unlock();
        }
    }

和 HashEntry 构造函数

/**
     * ConcurrentHashMap list entry. Note that this is never exported
     * out as a user-visible Map.Entry.
     *
     * Because the value field is volatile, not final, it is legal wrt
     * the Java Memory Model for an unsynchronized reader to see null
     * instead of initial value when read via a data race.  Although a
     * reordering leading to this is not likely to ever actually
     * occur, the Segment.readValueUnderLock method is used as a
     * backup in case a null (pre-initialized) value is ever seen in
     * an unsynchronized access method.
     */
    static final class HashEntry<K,V> {
    final K key;
            final int hash;
            volatile V value;
            final HashEntry<K,V> next;

            HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
                this.key = key;
                this.hash = hash;
                this.next = next;
                this.value = value;
            }

把实现

tab[index] = new HashEntry<K,V>(key, hash, first, value);

我对HashEntry的评论感到困惑,作为JSR-133,一旦构建了HashEntry,所有最终字段都将对所有其他线程可见,字段是易失性的,所以我认为它对其他线程也是可见的???.另一点,他所说的重新排序是:HashEntry对象引用可以在完全构造之前分配给tab[...](所以结果是其他线程可以看到这个条目,但e.value可以是空的)?

更新:我读了这篇文章,它很好。但是我需要关心这样的案例吗?

ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();

thread1:

Person p=new Person("name","student");        
queue.offer(new Person());

thread2:
Person p = queue.poll();

线程 2 是否有可能收到未完成构造的 Person 对象,就像 HashEntry 中的一样

tab[index] = new HashEntry(key, hash, first, value); ?


答案 1

对于那些对Doug Lea关于这个话题的答案感兴趣的人,他最近解释了原因readValueUnderLock

这是对有人提出以下问题的回应:

在 ConcurrentHashMap 中,get 方法不需要“readValueUnderLock”,因为竞速删除不会使值为 null。该值永远不会在 from 删除线程上变为 null。这意味着即使删除线程(在同一键上)已经进行到克隆列表的前面部分为止,也可以返回键的值。只要是所需的效果,这很好。

但这意味着NEW内存模型不需要“readValueUnderLock”。

但是,对于旧内存模型,由于重新排序(很少但可能),看跌期权可能会看到值 null。

我的理解是否正确。

响应:

差一点。你是对的,它永远不应该被调用。但是,JLS/JMM 可以被解读为不是绝对禁止调用它,因为在构造函数中设置的 finals 与 volatiles 之间所需的排序关系中存在弱点(键是 final,值是易失性的),使用入口对象对线程进行读取。(在 JMM-ese 中,finals 的排序约束不在同步关系之外。这就是文档评论(粘贴在下面)所指的问题。从来没有人想过处理器/编译器可能会发现任何实际漏洞来产生空值读取,并且可以证明不存在(也许有一天JLS / JMM修订版会填补空白以澄清这一点),但Bill Pugh曾经建议我们无论如何都要把它放在保守的正确性上。回想起来,我不太确定这是一个好主意,因为它导致人们提出异国情调的理论。

都可以在这里查看


答案 2

我对HashEntry的评论感到困惑,因为JSR-133,一旦构建了HashEntry,所有最终字段都将对所有其他线程可见,值字段是易失性的,所以我认为它对其他线程也是可见的???.

其他线程也将看到价值,但是...条目(进入 Object[])的分配是在初始化后和锁定下完成的。因此,如果任何线程看到,它将尝试读取锁定下的值。null

另一点,他所说的重新排序是:HashEntry对象引用可以在完全构造之前分配给tab[...](所以结果是其他线程可以看到这个条目,但e.value可以是空的)?

不,它不能 b/c 存在易失性赋值 () 和 than 意味着所有其他操作必须在手之前设置(即不重新排序)。还要记住,java对象的创建是两个阶段,创建一个带有零/空字段的空对象(如使用默认的c-tor),然后调用方法(这是构造函数)。在完成构造函数调用及其最后一次赋值之前,不能将对象分配给任何内容(以确保正确的排序,也称为 happen-before)value<init>value


推荐