【问题标题】:Avoiding ArrayList Concurrency with multiple threads避免多线程的 ArrayList 并发
【发布时间】:2017-10-05 19:09:12
【问题描述】:

我有一个 ArrayList 对象:

    List<Sample> dataList = new ArrayList<Sample>();

这有一个 Sample 对象的列表。示例包含一个长时间戳和一个双值原语。

我有一个程序可以通过多个线程对这些进行操作。我有一个线程将修剪数据 1/hr。修剪大约需要 2 分钟才能完成(低端嵌入式系统和大量数据)。它调用以下函数来执行此操作:

 public synchronized void prune(long timestamp)
    {
        Iterator<Sample> it = dataList.listIterator();

        while (it.next().getTimestamp() < timestamp)
        {
            it.remove();
        }       

    }
}

我还通过另一个线程以 1/秒的速度将动态数据更新到该数组。根据添加的数据,它可以调用以下两个函数之一:

  public synchronized void addPointData(ArrayList<Sample> a)
    {

            a.addAll(dataList);
            dataList = a;

    }

    public synchronized void addPointData(Sample a)
    {

            dataList.add(a);
            if (dataList.size() > 0 && pruneLock == 0 && dataList.get(0).getTimestamp() < (System.currentTimeMillis() - 90000000L) * 1000000)
            {
                dataList.remove(0);
                startTimestamp = dataList.get(0).getTimestamp();
            }       
    }

到目前为止,我没有遇到任何并发异常,我不相信我有任何丢失的数据。如果修剪器让添加功能等待它,我担心会丢失数据。谁能解释为什么我没有例外?我应该以不同的方式来做这件事吗?

【问题讨论】:

  • 看起来您实际上只是将其用作队列。有很多正确的并发队列结构。
  • 嘿路易斯,这是趋势应用程序的一部分。数组列表中的数据可以在某些情况下扩展并锁定,修剪器必须等待 24 小时才能再次触摸它。
  • 是的,然后呢?
  • 还有内存限制(在内存存储中),使用数组列表到底有什么问题。为什么我要使用队列?
  • 你这里的方法都没有做到这一点。如果情况确实如此,则可能需要使用列表,但我会注意到 remove(0) 来自 ArrayList 会非常昂贵,因为它是 O(n)。

标签: java multithreading arraylist


【解决方案1】:

由于没有其他人回答...好的 cmets,以及有关如何更有效地存储数据的要点。但除此之外,如果您包含的操作是修改列表的 only 操作,则您的代码正确,以防止并发修改异常或数据丢失。所有修改的操作都是同步的。您会遇到阻塞情况,但不会有并发修改。

让添加函数等待修剪完成正是应该发生的事情。只要没有其他原因 add 函数等不及,就可以。

不过,正如评论者所指出的,有更快的方法来解决这个问题,这可能会减少整体等待时间。鉴于您总是按时间删除,如果您知道按时间顺序添加内容,则可以显着优化流程。如果您按时间排序并要求这样做,那么肯定有更好的选择(或者如果您可以选择在插入时排序)。 Java 8 流的一些用途可以并行化并提供一些不同的处理选项。

但简短的回答是,您已经在需要的地方锁定以防止出现问题。

【讨论】:

  • 由于修剪和添加是按计划进行的(添加为 1/秒),如果它等待,它不会阻止数据被收集,它只会阻止它被写入正确吗?
  • 正确。这就是同步的行为,阻塞并等待直到没有其他人锁定它。
【解决方案2】:

您将synchronize 放在与您的ArrayList 相关的所有内容上这一事实将确保您不会遇到并发问题。

另一方面,你的表现会很糟糕。每次修剪发生时,所有需要数据列表的东西都会停止。

您有两个大的性能问题。首先是ArrayList上的remove效率非常低。每次你从中间移除一些东西时,它必须把上面的所有东西都洗掉以填补空白。这甚至可以容忍的唯一原因是因为它使用了System.arrayCopy,这是一个低级别的超级优化调用并且速度很快。但是,如果您进行大量删除操作,那么每次删除操作都会向下移动。

不清楚的一点是您的样本是否已排序。如果它们是有序的,并且您可以确定需要修剪的开始和结束位置,则应使用removeRange 一次性删除块。

如果您的列表已排序并且您要从前面删除,则最好使用ArrayDeque,因为这可以有效地支持从前面和后面删除。

假设情况并非如此,并且时间戳是随机分布在数组中的,那么使用以下方法可能会更快地填补空白:

j = 0;
for (int i = 0; i < dataList.size(); i++) {
    Sample s = dataList.get(i);
    if (s.getTimestamp() >= timestamp) {
        dataList.set(j++, s);
    }
}
removeRange(j, dataList.size());

我还没有测试过,但也许你明白了。

或者也许有一些 Java 8 的聪明之处可以更优雅地做同样的事情。

但这仍然会在修剪发生时锁定您的整个数据结构,因此您可以考虑以较小的块进行修剪。这将在更短的时间内同步您的数据并缩短延迟时间。

【讨论】:

  • 已排序。而且我只从后端(最旧的)删除。通常通过删除 (0) 或通过 prune 从后面删除一个块。
  • 典型情况下 add 函数中的 remove (0) 将处理大多数项目,因此 prune 不必做任何事情。在奇怪的情况下,它将是一大块样本。最多 50% 的数组列表
  • 如果你指的是第一个元素(元素 0),那么你最好使用 Deque 实现(例如,ArrayDeque)。这允许从两端有效去除。我更新了答案。
  • 这是典型的 PRUNE 功能,而不是整个程序。您可以并且将在前面添加单曲,在前面添加块,迭代并获取时间戳 x 和 y 之间的元素。最后一个我认为最好不要使用队列。你同意吗?
  • 如果你在前面添加元素(意味着元素零),那么你绝对应该使用队列。使用 ArrayDeque 并没有太多损失,因为 ArrayList 之上的开销很小。
猜你喜欢
  • 2015-05-18
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2018-04-12
  • 1970-01-01
  • 2017-09-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多