ConcurrentHashMap 中 entrySet().removeIf 的行为

Posted

技术标签:

【中文标题】ConcurrentHashMap 中 entrySet().removeIf 的行为【英文标题】:Behavior of entrySet().removeIf in ConcurrentHashMap 【发布时间】:2015-07-04 17:53:51 【问题描述】:

我想使用 ConcurrentHashMap 让一个线程定期从地图中删除一些项目,而其他线程同时从地图中放置和获取项目。

我在删除线程中使用map.entrySet().removeIf(lambda)。我想知道我可以对它的行为做出什么假设。我可以看到removeIf 方法使用迭代器遍历地图中的元素,检查给定条件,然后在需要时使用iterator.remove() 删除它们。

文档提供了一些关于 ConcurrentHashMap 迭代器行为的信息:

类似地,迭代器、拆分器和枚举返回元素 反映哈希表在某个时刻或之后的状态 创建迭代器/枚举。嘿,不要抛出 ConcurrentModificationException。但是,迭代器被设计为一次只能由一个线程使用。

由于整个removeIf 调用发生在一个线程中,我可以确定迭代器当时没有被多个线程使用。我仍然想知道下面描述的事件过程是否可能:

    映射包含映射:'A'->0 删除线程开始执行map.entrySet().removeIf(entry->entry.getValue()==0)removeIf调用中删除线程调用.iteratator()并获取反映集合当前状态的迭代器 另一个线程执行map.put('A', 1) 删除线程仍会看到'A'->0 映射(迭代器反映旧状态)并且因为0==0 为真,它决定从映射中删除A 键。 地图现在包含'A'->1,但删除线程会看到0 的旧值,并且'A' ->1 条目被删除,即使它不应该被删除。地图是空的。

我可以想象这种行为可以通过多种方式被实现所阻止。例如:迭代器可能不反映放置/删除操作,但总是反映值更新,或者迭代器的 remove 方法在调用键上的 remove 之前检查整个映射(键和值)是否仍然存在于映射中。我找不到有关发生的任何这些事情的信息,我想知道是否有什么东西可以使该用例安全。

【问题讨论】:

在所有java集合中,迭代器都是由原始数据集支持的,所以数据的变化会立即被迭代器反映,这通常会抛出ConcurrentModificationException。并发集合是不同的,因为迭代器是故障安全的,这意味着它们可以处理对底层集合的修改。但是,它们仍然在大多数实现中迭代实时数据。 如here 所述,唯一可以保证的是迭代器所遍历的元素在迭代器构建时存在于映射中。这些元素可能不再存在,或者在您使用迭代器访问它们时可能会被更改。我的建议是您仅将迭代器用作可能性指示,但直接在地图上执行所有操作(例如,使用map.remove(key,value) 而不是iterator.remove())。 【参考方案1】:

我还设法在我的机器上重现了这种情况。 我认为,问题在于EntrySetView(由ConcurrentHashMap.entrySet() 返回)从Collection 继承其removeIf 实现,它看起来像:

    default boolean removeIf(Predicate<? super E> filter) 
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) 
            // `test` returns `true` for some entry
            if (filter.test(each.next()))  
               // entry has been just changed, `test` would return `false` now
               each.remove(); // ...but we still remove
               removed = true;
            
        
        return removed;
    

以我的拙见,这不能被视为ConcurrentHashMap 的正确实现。

【讨论】:

【参考方案2】:

在 Zielu 的回答下方与 cmets 中的用户 Zielu 讨论后,我更深入地研究了 ConcurrentHashMap 代码并发现:

ConcurrentHashMap 实现提供remove(key, value) 方法调用replaceNode(key, null, value) replaceNode 在删除之前检查键和值是否仍然存在于映射中,因此使用它应该没问题。文档说它

用 v 替换节点值,以 cv 匹配为条件 if * 非空。

在问题中提到的情况下,ConcurrentHashMap 的.entrySet() 被调用,它返回EntrySetView 类。然后removeIf 方法调用.iterator() 返回EntryIteratorEntryIterator 扩展 BaseIterator 并继承 remove 实现,该实现调用 map.replaceNode(p.key, null, null) 禁用条件删除并始终删除密钥。

如果迭代器总是迭代“当前”值并且在修改某些值时从不返回旧值,则仍然可以防止事件的负面过程。我仍然不知道这是否发生,但下面提到的测试用例似乎验证了整个事情。

我认为这已经创建了一个测试用例,表明我的问题中描述的行为确实可以发生。如果我在代码中有任何错误,请纠正我。

代码启动两个线程。其中之一 (DELETING_THREAD) 删除映射到“假”布尔值的所有条目。另一个 (ADDING_THREAD) 随机将 (1, true)(1,false) 值放入映射中。如果它将true 放入值中,则它期望该条目在检查时仍然存在,如果不是则抛出异常。当我在本地运行它时,它会很快抛出异常。

package test;

import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

public class MainClass 

    private static final Random RANDOM = new Random();

    private static final ConcurrentHashMap<Integer, Boolean> MAP = new ConcurrentHashMap<Integer, Boolean>();

    private static final Integer KEY = 1;

    private static final Thread DELETING_THREAD = new Thread() 

        @Override
        public void run() 
            while (true) 
                MAP.entrySet().removeIf(entry -> entry.getValue() == false);
            
        

    ;

    private static final Thread ADDING_THREAD = new Thread() 

        @Override
        public void run() 
            while (true) 
                boolean val = RANDOM.nextBoolean();

                MAP.put(KEY, val);
                if (val == true && !MAP.containsKey(KEY)) 
                    throw new RuntimeException("TRUE value was removed");
                

            
        

    ;

    public static void main(String[] args) throws InterruptedException 
        DELETING_THREAD.setDaemon(true);
        ADDING_THREAD.start();
        DELETING_THREAD.start();
        ADDING_THREAD.join();
    

【讨论】:

很好的发现,不确定这是否一定是 JDK 中的错误,但绝对令人惊讶。可能值得在 JSR 166 邮件列表中打开票证或向开发人员发送消息。 @JohnVint 几天前我在bugreport.java.com 提交了一份错误报告,目前正在审核中。 我认为@Vladimir S. 错误报告是bugs.java.com/bugdatabase/view_bug.do?bug_id=8078645。看起来该错误已在 Java 9 (b65) 中修复。

以上是关于ConcurrentHashMap 中 entrySet().removeIf 的行为的主要内容,如果未能解决你的问题,请参考以下文章

Java并发容器--ConcurrentHashMap

ConcurrentHashMap 源码浅析 1.7

java多线程:并发包中ConcurrentHashMap和jdk的HashMap的对比

HashMap与ConcurrentHashMap

ConcurrentHashMap 1.7和1.8区别

ConcurrentHashMap