多线程库,暴露不安全的ArrayList。

5

我正在使用Java中的一个共享库,它返回ArrayList。当我遍历它时,可能会抛出ConcurrentModificationException异常,我希望能有100%保证安全。我考虑了以下内容,欢迎任何建议。

data_list是从MT库返回的ArrayList<>。

boolean pass = true;

ArrayList<Something> local = new ArrayList<Something>(256);

for (int spin=0; spin<10; ++spin)
{
  try {
    local.addAll(data_list);
  }
  catch (java.util.ConcurrentModificationException ce) {
    pass = false;
  }
  finally {
    if (pass) break;
    pass = true;
  }
}

假设变量passtrue,我应该如何操作本地?

5
一个多线程库暴露了一个非并发的集合,并在暴露后继续修改它?我认为最好的选择是切换到另一个库。 - Theodoros Chatzigiannakis
2
虽然这不是你所问问题的答案,但如果你要将一个ArrayList复制到另一个ArrayList中,通常最好将目标ArrayList的初始大小设置为与源ArrayList相同的大小,而不是使用一个常数,例如:local = new ArrayList<>(data_list.size()); 而不仅仅是使用256。 - Jules
我同意@Theodoros的观点,你确定你没有以错误的方式使用库吗?这似乎是库本身设计不良。 - cjstehno
你们所有人都是正确的,但出于某种原因我必须使用这个暴露了不安全ArrayList的MT库,我只是试图想出一些模式,至少减少并发异常的概率;所以我正在迭代本地副本,而异常可能会在AddAll中发生,我“旋转”几次,这使我相信我处于更好的概率空间;(但别误解我的意思,如果我是一名喷气式战斗机飞行员,而这是其中一个算法,我不会飞它!) - Basixp
@Basixp:你应该告诉我们你正在使用哪个库,以及你是如何调用它的。 - Steven Schlansker
4个回答

9

这样做没有安全的方式。 您不应该捕获ConcurrentModificationException

此类的iterator和listIterator方法返回的迭代器是快速失败的:如果在创建迭代器后,除了通过迭代器自己的remove或add方法之外,以任何方式对列表进行了结构性修改,则迭代器将抛出ConcurrentModificationException异常。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是冒着在未来的某个不确定的时间冒险产生任意的、非确定性的行为。

请注意,迭代器的快速失败行为不能得到保证,因为通常情况下,在未同步的并发修改存在的情况下,无法作出任何硬性保证。快速失败的迭代器尽最大努力抛出ConcurrentModificationException异常。因此,编写依赖于此异常的程序来确保其正确性是错误的:迭代器的快速失败行为只能用于检测错误。

有些集合,比如HashMap甚至在这种使用方式下会进入无限循环。这里有一个解释它发生的原因

你不应该这样做。没有正确的做法。

要么你误解了库的工作原理,要么你需要换一个由能干的开发人员编写的库。

你正在使用哪个库?


0

您没有明确定义“安全”的含义,也没有指定对列表进行了哪些修改,但在许多情况下,手动按索引迭代可能是可以接受的,即:

for (int index = 0; index < data_list.size(); index ++)
    local.add(data_list.get(index));

在我看来,有四种可能的修改方式,其可接受程度各不相同:

  • 可以添加新项目。只要列表没有增长到触发后备列表扩展(由于指数递减频率,如果发生重试应保证最终成功),这个解决方案应该适用于此情况。
  • 可以修改现有项目。这个解决方案可能无法在任何给定时间提供列表内容的一致视图,但它将保证提供一个可用的列表,代表已经在列表中的项目,这可能是可以接受的,具体取决于您对“安全”的定义。
  • 可以删除项目。这个解决方案有很小的机会失败并出现IndexOutOfBoundsException,与修改项目的情况相同,一致性也会受到影响。
  • 可以将项目插入到列表中间。与修改项目的情况相同,还存在获取重复值的危险。追加情况下的后备数组扩展问题也会应用于此处。

0

你遇到了一个糟糕的情况,但我认为你的解决方案尽可能地可靠。新的ArrayList应该放在循环中,这样每次失败后都可以重新开始。实际上,最好的方法可能是将你的“try”行改成:

local = new ArrayList<Something>( data_list );

你不想让你的ArrayList在扩容时花费时间,因为这会阻碍你在列表变化之前获取数据。这样设置大小、创建和填充应该是浪费最少的努力。

你可能需要捕获除了ConcurrentModification以外的其他问题。你可能会从错误中学习到经验。或者只是catch Throwable。

如果你想走极端路线,可以将for循环内部的代码运行在自己的线程中,这样如果它挂起了,你就可以杀掉它并重新启动。这需要一定的工作量。

我认为这个方法会奏效,如果你让"spin"足够大的话。


-2

我没有任何根本性的改变,但我认为这段代码可以简化一下:

ArrayList<Something> local = new ArrayList<Something>(256);

for (int spin=0; spin<10; ++spin)
{
  try {
    local.addAll(data_list);
    break;
  }
  catch (java.util.ConcurrentModificationException ce) {}
}

-1,这可能会添加元素乘法(addAll不是原子操作),因此您可能会在结果集中得到重复的元素。 - Steven Schlansker
是的,但他要求以“安全”的方式完成,而这并不安全。 - Steven Schlansker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接