【问题标题】:IndexOutOfBoundsException when list.remove run in 2 thread the same time(concurrently)当 list.remove 在 2 个线程中同时运行(并发)时出现 IndexOutOfBoundsException
【发布时间】:2016-07-28 18:41:08
【问题描述】:

我在 main 方法中有一个列表,我想编写两个线程来使用这个列表。有时我会在同步块中捕获 IndexOutOfBoundsException(当线程调用 remove 方法时)。

主要方法:

public class PC {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        new Costumer("c1", strings).start();
        new Costumer("c2", strings).start();
        new Producer("p1", strings).start();
        new Producer("p2", strings).start();
        new Producer("p3", strings).start();
        new Producer("p4", strings).start();
    }
}

消费类:

class Costumer extends Thread {

    List<String> strings;
    public Costumer(String n, List<String> strings) {
        super(n);
        this.strings = strings;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (strings) {
                try {
                    if (strings.isEmpty()) {
                        strings.wait();
                    }
                    strings.remove(0); // <- where exception is thrown
                } catch (InterruptedException ex) {
                }
            }
        }
    }
}

生产者类:

class Producer extends Thread {

    List<String> strings;

    public Producer(String n, List<String> strings) {
        super(n);
        this.strings = strings;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (strings) {
                strings.add(String.valueOf(Math.random() * 1000));
                if (strings.size() == 1) {
                    strings.notify();
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
            }
        }
    }
}

堆栈跟踪:

Exception in thread "c2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
        at java.util.ArrayList.rangeCheck(Unknown Source)
        at java.util.ArrayList.remove(Unknown Source)
        at Costumer.run(PC.java:40)

【问题讨论】:

  • 这绝对不是您使用此代码时遇到的错误。你认为Arrays.asList 会返回什么?你读过它的 javadoc 吗?
  • 我还有另一个线程来生成字符串并将它们放入列表中。我总结了一下。在主代码中我有新的 Arraylist。
  • 这就是为什么你应该测试你自己的代码,如果它真的重现了问题。顺便说一句:我不认为这段代码可以重现这个问题,即使你已经修复了你的列表初始化。因此,还要发布通知您的线程的代码(因为这也可能重新填充该列表)。
  • 生产者将字符串添加到空列表时,我调用通知方法。
  • 我仍然认为您的代码无法重现此问题。如果你有一个生产者,那么它会通知一个客户,这不会造成任何问题。那么你能创建一个minimal reproducible example吗?

标签: java multithreading arraylist concurrency synchronization


【解决方案1】:

您的代码中的问题是 Costumer 类中的 if 测试必须用 while 循环替换,否则您可能会遇到竞争条件问题。事实上,假设我们有一个消费者在等待被通知,我们有一个消费者在等待字符串上的锁,我们有一个拥有字符串锁的生产者,它添加了一个新的字符串并调用 notify,因为我们没有更多的字符串。所以一旦它会释放锁,假设等待锁的消费者首先获得它(是的,不要忘记已经被通知的消费者仍然需要获取锁并且没有必要先获得锁),它然后删除一个字符串,然后第二个消费者(已经被消费者通知的那个)将从strings.wait()开始并调用strings.remove(0)而不检查它是否为空然后你会得到IndexOutOfBoundsException

换句话说,代码应该是这样的:

@Override
public void run() {
    while (true) {
        synchronized (strings) {
            try {
                while (strings.isEmpty()) {
                    strings.wait();
                }
                strings.remove(0);
            } catch (InterruptedException ex) {
            }
        }
    }
}

无论如何,将条件包装到 while 循环中是一种很好的做法,以避免出现像这样的奇怪错误。您可以在 ArrayBlockingQueue 之类的类中检查它是如何完成的,例如,所有条件都在 while 循环中检查。

【讨论】:

  • “他们都会删除第一个字符串” 你为什么这么认为?因为你认为 OP 使用了不止一个生产者?
  • 为什么?我不明白。只有一个线程在通知中唤醒。通知文档:“唤醒正在此对象的监视器上等待的单个线程。”
  • @mohammad_1m2 这是一个竞争条件问题,我们可能有几种情况。这是一个:我们有一个消费者在等待被通知,我们有一个消费者在等待字符串上的锁,我们有一个拥有字符串锁的生产者,它添加了一个新的字符串并调用通知,因为我们没有更多的字符串。所以一旦它释放锁,假设等待锁的消费者首先获得它,然后删除一个字符串,然后已经通知的第二个消费者将从 strings.wait();并将调用 strings.remove(0);不检查它是否为空 => .IOOBE
  • @mohammad_1m2:尼古拉斯的回答是正确的,尽管可能没有足够的解释。调度程序不一定要选择通知线程接下来运行,有可能有一个顺序,通知线程直到另一个消费者已经消费了列表内容并将其留空之后才开始运行。您从等待中醒来(在此期间线程放弃监视器)之后对状态的任何假设都是毫无价值的。
  • @mohammad_1m2 在我的例子中你不明白的是,我们只有一个消费者线程等待通知,另一个消费者只等待变量字符串的锁定。当生产者调用 notify 时,它会释放等待通知的消费者,但该线程仍然需要获取变量字符串上的锁,所以如果其他消费者先获得它,您将收到错误,因为已通知的消费者没有'不要先重新检查条件。
猜你喜欢
  • 2010-10-20
  • 1970-01-01
  • 2019-01-01
  • 2012-09-04
  • 2013-04-12
  • 1970-01-01
  • 2020-12-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多