同时迭代Java ArrayList时发生NoSuchElementException

时间:2018-07-25 13:42:06

标签: java arraylist

我有一种类似于下面的方法:

std::initializer_list<const char*>

当同一用户(另一个实例)同时运行时,有时会抛出template <typename T> void f(T); f({ "narrow string" }); 。根据我的理解,有时T在列表中没有元素时会执行。仅在访问时会发生这种情况。方法同步会解决这个问题吗?

堆栈跟踪为:

{{ constant('class', exception) is constant('\\Symfony\\Component\\HttpKernel\\Exception\\HttpException') }}

此堆栈跟踪在public void addSubjectsToCategory() { final List<Subject> subjectsList = new ArrayList<>(getSubjectList()); for (final Iterator<Subject> subjectIterator = subjectsList.iterator(); subjectIterator.hasNext();) { addToCategory(subjectIterator.next().getId()); } } 行处失败。

7 个答案:

答案 0 :(得分:9)

迭代器的基本规则是在使用迭代器时不得修改基础集合。

如果您只有一个线程,那么只要getSubjectsList()不返回null或addToCategory()getId()有一些奇怪的副作用,那么这段代码似乎就没有问题会修改subjectsList。但是请注意,您可以将for循环重写得更好一些(for(Subject subject: subjectsList) ...)。

根据您的代码判断,我的最佳猜测是您还有另一个线程正在其他地方修改subjectsList。在这种情况下,使用SynchronizedList可能无法解决您的问题。据我所知,同步仅适用于List方法,例如add()remove()等,并且在迭代过程中不锁定集合。

在这种情况下,将synchronized添加到方法中将不会有任何帮助,因为另一个线程正在其他地方执行其令人讨厌的工作。如果这些假设是正确的,那么最简单,最安全的方法是制作一个单独的同步对象(即Object lock = new Object()),然后在该for循环以及程序中修改该对象的任何其他位置放置synchronized (lock) { ... }。采集。这样可以防止其他线程在该线程迭代时进行任何修改,反之亦然。

答案 1 :(得分:3)

听起来好像另一个线程正在调用该方法并在另一个线程即将获取下一个元素的同时获取最后一个元素。因此,当另一个线程完成并返回到暂停的线程时,将一无所有。我建议使用ArrayBlockingQueue而不是列表。当一个线程已经迭代时,它将阻塞线程。

public void addSubjectsToCategory() {
    final ArrayBlockingQueue<Subject> subjectsList = new ArrayBlockingQueue(getSubjectList());
    for (final Iterator<Subject> subjectIterator =
         subjectsList.iterator(); subjectIterator.hasNext();) {
        addToCategory(subjectIterator.next().getId());
    }
}

您可能需要消除一些皱纹。 ArrayBlockingQueue将为空或已满,然后分别等待线程插入或取出某物,然后再解除阻止并允许其他线程访问。

答案 2 :(得分:3)

subjectIterator.hasNext();) {

---想象一下,此时在调用hasNext()next()方法之间发生了线程切换。

addToCategory(subjectIterator.next().getId());

假设您位于列表中的最后一个元素,将会发生以下情况:

  • 线程A调用hasNext(),结果为true;
  • 线程切换到线程B;
  • 线程B调用hasNext(),结果也为true;
  • 线程B调用next()并从列表中获取下一个元素;现在列表是空的,因为它是最后一个;
  • 线程切换回到线程A;
  • 线程A已经在for循环的主体内,因为这是它被中断的地方,它之前已经调用了hasNext, 是真的
  • 因此线程A调用next(),由于列表中没有其他元素,该调用现在由于一个异常而失败。

因此,在这种情况下,您要做的是使操作hasNextnext以原子方式运行,而不会在两者之间发生线程切换。 列表上的简单同步确实可以解决问题:

public void addSubjectsToCategory() {
    final ArrayBlockingQueue<Subject> subjectsList = new ArrayBlockingQueue(getSubjectList());
    synchronized (subjectsList) {
        for (final Iterator<Subject> subjectIterator =
            subjectsList.iterator(); subjectIterator.hasNext();) {
                addToCategory(subjectIterator.next().getId());
        }
    }
}

但是,请注意,此方法可能会影响性能。在迭代结束之前,其他线程将无法从同一列表读取或写入同一列表(但这是您想要的)。为了解决这个问题,您可能需要在hasNext和next附近的循环内移动同步。或者,您可能想使用更复杂的同步机制,例如读写锁。

答案 3 :(得分:2)

如果您需要的只是简单的调用Sycnchronization,则可以使用Collections.synchronizedList(list)。但是请注意,您使用的迭代器必须在Synchronized块内。

答案 4 :(得分:2)

据我所知,您正在将元素添加到可能正在读取过程中的列表中。 假设列表为空,而您的其他线程正在读取它。这些问题可能会导致您遇到问题。您永远无法确保以这种方式将元素写入到要尝试读取的列表中。

答案 5 :(得分:2)

我很惊讶没有看到涉及使用CopyOnWriteArrayList或番石榴的ImmutableList的答案,所以我想在这里添加这样的答案。

首先,如果您的用例相对于多次读取仅增加了一些内容,请考虑使用CopyOnWriteArrayList解决并发列表遍历问题。方法同步可以解决您的问题,但是根据该类的Javadoc,如果“大量”并发访问的次数超过了写入次数,则CopyOnWriteArrayList可能会具有更好的性能。

第二,如果您的用例是如此,您可以以单线程方式将所有内容预先添加到列表中,然后才需要同时对其进行迭代,则考虑使用Guava的ImmutableList类。您可以通过首先使用标准ArrayListLinkedListbuilder for your ImmutableList来完成此操作。一旦完成单线程数据输入,就可以使用ImmutableList.copyOf()ImmutableList.build()实例化ImmutableList。如果您的用例允许这种写/读模式,那么这可能是您性能最高的选择。

希望有帮助。

答案 6 :(得分:0)

考虑到这是一个并发问题,我想提出一个可能解决您问题的建议。

如果使方法 addSubjectsToCategory()同步可以解决您的问题,则您已找到并发问题所在的位置。确定发生问题的位置很重要,否则您提供的信息对我们无用,我们无法为您提供帮助。

如果在您的方法中使用同步可以解决您的问题,那么可以将此答案视为有教益或更优雅的解决方案。否则,请在实现线程环境的地方共享代码,以便我们看看。

public synchronized void addSubjectsToCategory(List subjectsList){
  Iterator iterator = subjectsList.iterator();
  while(iterator.hasNext())
    addToCategory(iterator.next().getId());
}

//This semaphore should be used by all threads. Be careful not to create a
//different semaphore each time.
public static Semaphore mutex = new Semaphore(1);

public void addSubjectsToCategory(List subjectsList){
  Iterator<Subject> iterator = subjectsList.iterator();
  mutex.acquire();
  while(iterator.hasNext())
    addToCategory(iterator.next().getId());
  mutex.release();
}

Synchronized是干净,整洁且优雅的。您有一个非常小的方法,可以创建锁,不需要imho。 已同步意味着一次只能有1个线程进入该方法。这意味着,仅当您希望每次有1个线程处于活动状态时才应使用它。

如果您实际上需要并行执行,那么您的问题与线程无关,但是与其余代码有关,我们看不到。