【问题标题】:Java iterator-like construct for concurrent map modification用于并发映射修改的类似 Java 迭代器的构造
【发布时间】:2016-11-08 15:45:15
【问题描述】:

假设我做了类似的事情:

for (X x : some_map.values ())
    doSomething (x);

其中doSomething() 间接通过几层代码为some_map 添加了更多值。使用迭代器(如上面的示例代码),我得到了ConcurrentModificationException

我可以将some_map 设为LinkedHashMap,即具有可预测的迭代顺序。此外,当向其中添加新项目时,它总是在迭代顺序结束时添加。换句话说,如果ConcurrentModificationException 以某种方式没有 被抛出,则循环只会在最后遍历新添加的项目,即可以正常工作。或者,换一种说法,我确实有这里有一个并发修改,但我可以保证它的行为是明确定义的,没有错误。

问题:我可以在上述循环中使用与迭代器“相似”的东西来避免并发修改异常吗?

请注意,由于一些额外的限制,我无法指出添加项目时知道循环。我也无法将其更改为不是地图的东西。这只是一段代码,但some_map 也在其他地方使用,是一个地图是有原因的。

编辑: 我的问题是我是否可以迭代(不一定以标准方式)我添加项目的同一张地图。很明显,我可以改为迭代副本,在循环将所述副本与原始副本进行比较以找到新项目后,迭代这些项目等等。问题是,我可以完全避免这种情况,因为在我的情况下,唯一的问题是过度急切地抛出 ConcurrentModificationException。回答“不,你不能”对我来说比“你可以做......而不是”更好,因为我可以自己为此设计一个替代代码。我只是想知道我是否忽略了一些优雅的解决方案。

【问题讨论】:

  • 您可以跟踪应该添加哪些元素,然后在这些元素上重新运行该函数,直到没有更多元素要添加。完成所有迭代后,您可以按顺序添加所有新元素。
  • 如果同时添加了一个新值,如果同时迭代这些值,您希望能够直接看到它吗?
  • @marstran:这不是一个选项,因为添加项目的地方对循环一无所知,因此不能有任何解决方法(比如添加到其他集合)。
  • 澄清一下:如果doSomething() 向地图添加了一个条目,您是否希望您的循环也针对该值进行迭代?即你希望你的迭代“看到”你的地图的变化。如果是这样,如何修改条目的值和删除?

标签: java concurrentmodification


【解决方案1】:

当处理涉及删除元素时,我用来在迭代列表时处理列表的一个技巧是使用递减索引访问它并在最后删除项目。

for(int i=myList.size()-1;i>=0;i--) {
        Object item = myList.get(i);
        if(needsToBeRemoved(item)) {
            myList.remove(i);
        }
}

通过这种方式,您可以在迭代列表时对其进行操作。这对列表有效,因为除了迭代器之外,您还可以通过索引访问其元素。

您也可以在处理地图时应用相同的技术,如果它遵循某种在处理时不会改变的排序(有序地图)。

如果您想要或需要使用迭代器,那么除了复制信息(使用第二张地图)之外别无选择

更新

您还可以使用辅助迭代器(键列表)。例如:

public static void main(String[] args) {
        Map<Long, String> map = new HashMap<>();
        map.put(1L, "Start");
        map.put(10L, "End");

        // This throws ConcurrentModificationException
        // for (Long value : map.keySet()) {
        // map.put(value + 1, "Other");
        // }
        for (Long value : new ArrayList<Long>(map.keySet())) {
            // This works ok
            map.put(value + 1, "Other");
        }

        System.out.println(map);
        // Prints: {1=Start, 2=Other, 10=End, 11=Other}
    }

【讨论】:

  • 我有一张地图,而不是一张清单。我无法从地图转换它,因为它需要是用于其他目的的地图。
【解决方案2】:

这可能有点争议,但如果你不关心并发修改,为什么不使用LinkedHashMap而忽略ConcurrentModificationException

基本上:

try {
   myMap.values().forEach(this::doSomething);
}
catch (ConcurrentModificationException ignored) {
}

我认为这适用于LinkedHashMap,但它显然不是预期用途。但是,您可以实现自己的用于此类用途的版本。

“正确”的方法是复制要迭代的值或键,然后检查是否添加了某些内容。大致:

final Set<K> processedKeys = new HashSet<>();

do {
   final Set<K> keysToProcess = new HashSet<>(myMap.keySet());
   keysToProcess.removeAll(processedKeys);
   keysToProcess.forEach(key -> doSomething(myMap.get(key)));
} while (!keysToProcess.isEmpty());

更新 @doublep 和 @RealSkeptic - 为什么我认为忽略 myMap.values().forEach(this::doSomething); 中的异常会起作用。

请查看forEach的代码或values()返回的集合在LinkedHashMap中:

    public final void forEach(Consumer<? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

所以forEach 遍历链表。如果将值添加到映射中,它们将被添加到列表中,因此for 循环将迭代直到到达链表的末尾。首先然后该方法检查修改计数。因此,该操作将有效地应用于所有值,甚至是新添加的值。

【讨论】:

  • 忽略异常有什么帮助?你只会停留在迭代器中的同一个地方,不是吗?
  • 在第一个示例中,我很确定在抛出 ConcurrentModificationException 之后的值将永远不会被循环处理,这是完全不正确的。关于第二个,谢谢,但这不是问题所在。是的,我知道我可以这样重写,但请阅读编辑。
  • @RealSkeptic 查看我的更新。不,你不会。 forEach 仅在迭代结束时检查修改。但是,这是特定于实现的。
  • @lexicore:你是对的,forEach() 是不同的。但我不想依赖可以被视为实现细节的东西(即它是只在最后抛出还是在每次迭代后检查)。我想我会赞成这是一个有效的答案,但我不会接受它;)
  • @doublep 好吧,您已经要求“不一定以标准方式”,所以就这样吧。 :)
【解决方案3】:

迭代索引而不是实际集合是鲜为人知的技巧,可以在这里完成这项工作。您可以使用流 API 获取第 n 个元素。由于创建了所有中间对象,我不确定这样做的效率有多高。

LinkedHashMap<String, String> lhm = new LinkedHashMap();        
// fill lhm
for ( int idx=0; idx < lhm.size(); idx++ ) {
    String val = lhm.values().stream().skip(idx).findFirst().get();
    // process val...
}

【讨论】:

  • 它将O(n)简单迭代转换为O(n²)。不是很好,即使在我的实践中n 通常并不大。
  • 不一定——但 values()、stream() 和 skip() 的实现需要优化并使用 LinkedHashMap 维护的列表。不过,我不确定情况是否如此。
  • 列表是链接的,不是随机访问的。因此,skip() 不能是O(1),而是O(n)
【解决方案4】:

没有内置方法,但如果您使用生产者/消费者模式的变体,每次迭代都会刷新处理队列:

Map<K, V> map; // assuming
Set<V> processed = new HashSet<>();
while (!map.values().containsAll(processed)) {
    List<V> queue = new ArrayList<>(map.values());
    queue.removeAll(processed);
    V x = queue.get(0);
    processed.add(x);
    doSomething(x);
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-15
    • 2014-09-12
    • 1970-01-01
    • 1970-01-01
    • 2011-01-16
    • 1970-01-01
    • 2014-09-20
    • 1970-01-01
    相关资源
    最近更新 更多