【问题标题】:Why is removing the 1st entry from a ConcurrentHashMap not immediately reflected in the iterator, but removing the 2nd or subsequent entries is?为什么从 ConcurrentHashMap 中删除第一个条目不会立即反映在迭代器中,但删除第二个或后续条目是?
【发布时间】:2020-08-09 22:28:36
【问题描述】:

我创建了一个 iterator(),然后在迭代之前从地图中删除了第一个条目。我总是得到从迭代器返回的第一个项目。但是当我删除第二个或后续条目时,当前迭代器不会返回该条目。

从地图中删除第一个条目的示例:

    Map<Integer,Integer> m1 = new ConcurrentHashMap<>();
    m1.put(4, 1);
    m1.put(5, 2);
    m1.put(6, 3);
    Iterator i1 = m1.entrySet().iterator();
    m1.remove(4);        // remove entry from map
    while (i1.hasNext())
        System.out.println("value :: "+i1.next()); //still shows entry 4=1

输出是:

value :: 4=1
value :: 5=2
value :: 6=3

从地图中删除第三个条目的示例:

    Map<Integer,Integer> m1 = new ConcurrentHashMap<>();
    m1.put(4, 1);
    m1.put(5, 2);
    m1.put(6, 3);
    Iterator i1 = m1.entrySet().iterator();
    m1.remove(6);        // remove entry from map
    while (i1.hasNext())
        System.out.println("value :: "+i1.next()); //does not show entry 6=3

输出是:

value :: 4=1
value :: 5=2

为什么从映射中删除第一个条目没有反映在迭代器中,但删除第二个或后续条目是?

Java documentation 说:

迭代器、拆分器和枚举返回的元素反映了在创建迭代器/枚举时或之后的某个时刻哈希表的状态。它们不会抛出 ConcurrentModificationException。

也就是说,它的迭代器反映了创建迭代器时哈希表的状态。 当我们在 Map 中添加或删除条目时,Iterator 会显示原始条目吗?

【问题讨论】:

    标签: java collections concurrenthashmap


    【解决方案1】:

    根据this,迭代器和拆分器是弱一致的。 “弱一致”的定义可以看here

    大多数并发集合实现(包括大多数队列) 也不同于通常的 java.util 约定,因为它们的 迭代器和拆分器提供弱一致而不是 快速失败遍历:

    • 它们可能与其他操作同时进行
    • 他们永远不会抛出 ConcurrentModificationException
    • 保证它们可以遍历 元素,因为它们在构造时只存在一次,并且可能(但是 不保证)反映之后的任何修改 建设

    这意味着在创建迭代器之后所做的任何修改都可能会被反映,但不能保证。这只是并发迭代器\拆分器的正常行为。

    【讨论】:

    • 第三点:它们保证遍历元素,因为它们在构造时就存在一次但是当我删除它时它没有显示第三个条目,那么它如何保证?
    • 第三个语句的要点是,在创建迭代器时存在于映射中的每个元素也将存在于该迭代器中,当且仅当没有对映射进行修改时在迭代器创建之后。如果更改地图,您可能会在遍历迭代器时看到更改,也可能看不到更改。结果未指定,此处不提供任何保证。
    • 更准确地说,它保证了happens-before关系。这意味着迭代器将反映另一个线程迭代器创建之前对地图所做的任何更改。但不保证 迭代器创建之后所做的修改。
    【解决方案2】:

    要实现恰好一次迭代行为,当您通过 Iterator 对象删除元素时,需要更新迭代器数据结构以使其与集合发生的情况保持同步。这在当前实现中是不可能的,因为它们不保持与未完成迭代器的链接。如果他们这样做了,他们将需要使用引用对象或冒着内存泄漏的风险。

    保证迭代器在创建地图时反映地图的状态。进一步的更改可能会反映在迭代器中,但不一定非要如此。

    将 Iterator 视为一个 LinkedList,并且您有头部参考。如果您碰巧从链表中删除了 head 并且没有重置 head.next 值并从 head 开始迭代,那么您仍然会从同一个 head 遍历,因为您使用的是过时的 head 引用。但是,当您删除非头部元素时,先前的 element.next 会更新。

    【讨论】:

    • 但是在这里,我不是通过 iterator() 删除元素,它只是从地图中删除。
    • @AkhileshDharDubey 更新了 ans 并尝试通过一些示例进行解释。我希望它有所帮助。
    【解决方案3】:

    答案在您引用的文档中:

    迭代器、拆分器和枚举返回反映哈希表在创建迭代器/枚举时或之后时的状态的元素。

    之后。迭代器可能会或可能不会显示自迭代器创建以来对地图的更改。

    设计人员在并发修改和迭代期间强制执行更精确的行为是不切实际的,但它没有被破坏。

    【讨论】:

      【解决方案4】:

      实际值检查在next()完成,

      public final Map.Entry<K,V> next() {
          Node<K,V> p;
          if ((p = next) == null)
              throw new NoSuchElementException();
          K k = p.key;
          V v = p.val;
          lastReturned = p;
          advance();
          return new MapEntry<K,V>(k, v, map);
      }
      

      advance 方法在可能的情况下前进,返回下一个有效节点,如果没有则返回 null。

      所以对于第一个条目K k = 4; V v = 1;,即使它已被删除。但是对于后续的k,v 将决定更新 fromadvance()

      所以如果你在remove 之后调用next(),它就不会出现(这很明显),

      Map<Integer,Integer> m1 = new ConcurrentHashMap<>();
      m1.put(4, 1);
      m1.put(5, 2);
      m1.put(6, 3);
      Iterator<Map.Entry<Integer, Integer>> i1 = m1.entrySet().iterator();
      m1.remove(4);        // remove entry from map
      i1.next();  
      
      while (i1.hasNext())
          System.out.println("value :: "+i1.next());
      

      【讨论】:

      • 但它在那里:value :: 4=1 value :: 5=2 value :: 6=3 怎么样,对于一种情况它存在,对于另一种情况它不存在?
      • 对于第一个条目K k = 4; V v = 1;,即使它已被删除。但是对于后续的k,v 将决定更新 fromadvance()
      • 好的,如果这是正确的,那么当我只删除第二个条目时,为什么迭代器也会更新。另外,解释一下 Advance() API?
      • 因为 后续的 k,v 将由 Advance() 的更新决定。您无需详细了解advance 方法。它返回下一个有效节点,如果没有则返回 null。如果您想了解详细信息,请查看java.util.concurrent.ConcurrentHashMap.Traverser#advance。我希望你明白了。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多