【问题标题】:ConcurrentHashMap reorder instruction?ConcurrentHashMap 重新排序指令?
【发布时间】:2023-03-05 13:44:01
【问题描述】:

我正在研究 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,所有 final 字段将对所有其他线程可见,value 字段是可变的,所以我认为它对其他线程可见也??? .另一点,他说的重新排序是:HashEntry 对象引用可以在完全构造之前分配给 tab[...] (所以结果是其他线程可以看到这个条目但 e.value 可以为空)?

更新: 我读了this 文章,很好。但是我需要关心这样的情况吗

ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();

thread1:

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

thread2:
Person p = queue.poll();

thread2 是否有可能收到一个未完成构造的 Person 对象,就像

中的 HashEntry

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

【问题讨论】:

  • 使用 volatile 值可以保证所有其他线程的可见性。
  • 是的,所以所有其他线程都可以看到所有字段,因此为什么我们只需要关心'value'的值是否为空?

标签: java multithreading concurrency


【解决方案1】:

对于那些对 Doug Lea 对此主题的回答感兴趣的人,他最近解释了readValueUnderLock 的原因

这是对有问题的人的回应:

在ConcurrentHashMap中获取 方法不需要 “readValueUnderLock”因为赛车 remove 不会使值为空。 该值永远不会在 从删除线程。这表示 get 有可能返回一个 键的值,即使删除 线程(在同一个键上)有 进行到克隆点 列表的前面部分。这 只要是想要的就可以了 效果。

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

但是对于 OLD 内存模型,一个 put 可能会看到值 null 由于 重新排序(罕见但可能)。

我的理解是否正确。

回复:

不完全是。你说得对 永远不应该被调用。然而 JLS/JMM 可以理解为不是绝对的 禁止它被调用 因为需要的弱点 决赛 vs 之间的排序关系 在构造函数中设置的 volatiles(关键是 最终,价值是不稳定的),wrt 使用条目由线程读取 对象。 (在 JMM-ese 中,排序 决赛的限制不在 同步关系。) 这就是文档评论的问题 (贴在下面)是指。没有人有 有没有想过任何实际的漏洞 处理器/编译器可能会发现 产生一个空值读取,它 可以证明不存在(并且 也许有一天 JLS/JMM 修订版 将填补空白以澄清这一点), 但是比尔·普格曾经建议我们把 无论如何,这只是为了 保守迂腐 正确的。回想起来,我不是这样 当然这是个好主意,因为它 引导人们想出异国情调 理论。

都可以查看here

【讨论】:

  • 谢谢,约翰!仍然不知道为什么规范不允许 volatile 字段在 c-tor 调用期间具有 final 语义。它们都需要实现内存屏障(因此在 c-tor 调用期间没有特殊的 volatile 屏障,如果 this 没有转义)。
  • @bestsss 我完全同意你的看法。我无法想象构造函数中易失性初始化的任何充分理由(甚至 Doug 本人)与 final 不同。
【解决方案2】:

我对 HashEntry 评论感到困惑,因为 JSR-133,一旦 HashEntry 是 构造,所有最终字段将是 对所有其他线程可见,值 领域是不稳定的,所以我认为 其他线程也可见??? .

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

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

不,它不能 b/c 存在易失性分配 (value),这意味着必须事先设置所有其他操作(即不重新排序)。另请记住,java 对象创建是 2 阶段,创建一个带有零/空字段的空对象(如使用默认 c-tor),然后调用 &lt;init&gt; 方法(这是构造函数)。在完成构造函数调用及其最后一次分配 value 之前,不能将对象分配给任何东西(以确保正确的排序,也称为发生前)

【讨论】:

  • 所以,HashEntry 被创建,所有字段都由 设置(volatile & final),然后对象被分配给 tab[..],我看不到任何情况 make e.value为空?
  • @secmask,我这样回答:我自己会省略检查,现在它需要一个额外的 cpu 时钟(因为 cpu 从未采用并正确预测分支)。任何人都可以拥有它。
【解决方案3】:

据我了解内存模型,对 volatile 变量的写入保证对于该变量的所有后续(由 同步顺序 定义)读取都是可见的。

然而,没有什么能保证在get() 中读取e.value 是在构造函数中写入value 之后(因为这些操作之间没有同步 关系),所以内存模型允许这种重新排序,并且在 null 值的情况下显式同步是必要的,以确保我们读取正确的值。

更新: 新的内存模型保证在写入 volatile 变量之前对非易失性变量的任何写入都可以在随后读取该易失性变量之后被其他线程看到,但反之亦然。

这是来自The Java Memory Model by Jeremy Manson, William Pugh and Sarita Adve的相关摘录:

5.1.1 锁定粗化。
...
所有这一切都只是一种迂回的说法,即访问普通变量 可以重新排序以下易失性读取或锁定获取,或前面 volatile write 或锁释放。这意味着可以移动正常访问 在锁定区域内,但(大部分)不在锁定区域内;

因此,构造对象的赋值可以通过在构造函数中写入 volatile 变量来重新排序,因此需要进行相关检查。

【讨论】:

  • 在设置 entry.value 之前,条目本身不应在 Object[] 中发布。在构造函数的值必须完成(并且不能重新排序)之前,检查是多余的 b/c 任何赋值。如果他们必须使其更明确应该构造条目,设置值,然后将条目设置到 Object[] 中(尽管现在它实际上以相同的方式编译)
  • @bestsss:你能通过引用 memody 模型来证明你的说法吗?
  • @axtavt,我试试旧的内存模型允许将易失性写入与非易失性读取和写入重新排序,这与大多数开发人员对易失性的直觉不一致,因此造成了混乱。 i> [前言部分];
  • ... 写入易失性字段与监视器释放具有相同的记忆效应,从易失性字段读取与监视器获取具有相同的记忆效应。实际上,由于新的内存模型对 volatile 字段访问与其他字段访问(无论是否为 volatile)的重新排序设置了更严格的限制,因此线程 A 在写入 volatile 字段 f 时可见的任何内容在线程 B 读取 f 时变得可见。 x
  • @axtavt,确实,锁定粗调:) 我认为没有任何实现将它与 volatiles 一起使用(根本没有性能优势),但这是一个很好的例子(synchronized 块也是如此)
猜你喜欢
  • 2021-12-02
  • 2016-12-27
  • 1970-01-01
  • 2019-01-15
  • 2019-03-09
  • 1970-01-01
  • 2021-01-16
  • 2011-05-01
  • 2015-12-11
相关资源
最近更新 更多