在迭代期间从collection.mutable.HashSet中删除元素是否安全?

时间:2018-02-06 22:38:49

标签: scala

实施了一个可变集合的retain方法as follows

def retain(p: A => Boolean): Unit =
  for (elem <- this.toList) // SI-7269 toList avoids ConcurrentModificationException
    if (!p(elem)) this -= elem

但是如果我实现了我自己的方法,它不会为迭代制作副本,那么什么都不会爆炸。

def dumbRetain[A](self: mutable.Set[A], p: A => Boolean): Unit = 
  for (elem <- self)
    if (!p(elem)) self -= elem

dumbRetain(mutable.HashSet(1,2,3,4,5,6), Set(2,4,6))
// everything is ok

我看到SI-7269的测试用例使用围绕java Set / Map的JavaConversions包装器,似乎问题来自底层java集合。

我知道永远不会有一个java集合传递给我的算法,所以我可以使用dumbRetain而不用担心ConcurrentModificationException吗?或者这是我不应该依赖的“巧合行为”?

编辑以澄清,我将使用dumbRetain作为算法中的实现细节,该算法可以完全控制传递给dumbRetain的内容。这将在单线程上下文中运行。

2 个答案:

答案 0 :(得分:1)

是的,你可以做到,只要你确定这是scala的本地HashSet实现,而不是java的包装......并且理解,这不是线程安全的,应该永远不会同时使用(原始的HashSet.retain也和其他的变异者一样)。

更好的是,只使用不可变Set.filter,除非你实际上有真实的证据(不仅仅是直觉)证明你的具体案例绝对需要可变容器。

答案 1 :(得分:1)

这似乎依赖于mutable.HashSet的具体实现,并且API中没有任何内容可以保证它适用于mutable.Set的所有其他实现,即使我们排除了所有其他包装器Java集合。

for - 循环

for (elem <- self) {
  ...
}

被移至foreachmutable.HashSet的实现方式如下:

override def foreach[U](f: A => U) {
    var i = 0
    val len = table.length
    while (i < len) {
      val curEntry = table(i)
      if (curEntry ne null) f(entryToElem(curEntry))
      i += 1
    }
}

本质上,它只是遍历底层Array的{​​{1}},并在每个元素上调用传递的函数FlatHashTable。整个f根本没有任何可以抛出任何东西的行,它根本不检查并发的 [footnote-1] 修改。

foreach似乎不那么令人不安:至少,你的程序失败很快,甚至返回一个详细的堆栈跟踪,指向问题发生的行。如果它只是在没有抛出任何东西的情况下恶化为未定义的行为,那实际上会更糟糕。这将是最糟糕的情况。但是,对于标准库中的集合,不应出现这种最坏的情况:Throw ConcurrentModificationException exception's in scala collections? #188

引用:

  

在scala / scala#5295中(合并到2.12.x)我确保删除从迭代器最后返回的元素不会导致迭代器出现问题。

因此,只要您在文档中明确指出只支持标准库中的集合,您很可能在自己的代码中使用它时没有任何问题。但是如果你在公共界面中使用它,这将是一个类似于你的问题中引用的“SI-7269”的错误的邀请。

[footnote-1] “并发”,如“ConcurrentModificationException”,而不是“并发执行的线程”。

编辑:我试图选择不那么含糊的配方。非常感谢@Dima的反馈和众多建议。