对单个静态整数索引的多次读写导致索引越界

Multiple read writes on a single static integer index causes index to go out of bounds

我正在尝试学习多线程,并正在尝试使用等待和通知的简单 producer/consumer 模式。当我将模式拆分为两个消费和一个生产时,我得到一个不清楚的 ArrayIndexOutOfBounds 异常。该问题并非总是发生,它有时会发生。我使用的是 I3 处理器。

我已经尝试添加一个 if 块来检查计数变量是低于还是高于声明的大小,但问题仍然存在。


    private static Object key = new Object();
    private static int[] buffer;
    private volatile static Integer count;

    static class Consumer {

        void consume() {
            synchronized (key) {
                if (isEmpty()) {
                    try {
                        key.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                buffer[--count] = 0;
                key.notify();
            }
        }

        public boolean isEmpty() {
            return count == 0;
        }
    }

    static class Producer {

        void produce() {
            synchronized (key) {
                if (isFull()) {
                    try {
                        key.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                buffer[count++] = 1;
                key.notify();
            }
        }

        public boolean isFull() {
            return count == buffer.length;

        }
    }
    public static void main(String[] args) throws InterruptedException {
        buffer = new int[10];
        count = 0;
        Producer producer = new Producer();
        Consumer consumer = new Consumer();
        Runnable produce = () -> {
            for (int i = 0; i < 1500; i++)
                producer.produce();
            System.out.println("Done producing");
        };
        Runnable consume = () -> {
            for (int i = 0; i < 1300; i++)
                consumer.consume();
            System.out.println("Done consuming");
        };
        Thread producerWorker = new Thread(produce);
        Thread consumerWorker = new Thread(consume);
        producerWorker.start();
        consumerWorker.start();
        //consumerWorker.join();
        Runnable checker = () -> {
            System.out.println("Lanched Delayed Consumer");
            for (int i = 0; i < 200; i++)
                consumer.consume();
        };
        Thread delayedConsumer = new Thread(checker);
        delayedConsumer.start();
        producerWorker.join();
        System.out.println("Data in Buffer " + count);
    }

 }

预计是:

启动延迟消费者
消费完毕
制作完成
缓冲区 0 中的数据

但得到了:

Lanched Delayed Consumer
Exception in thread "Thread-1" Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 10
    at com.multi.thread.waitandnotify.WaitNotifyRunner$Consumer.consume(WaitNotifyRunner.java:27)
    at com.multi.thread.waitandnotify.WaitNotifyRunner.lambda(WaitNotifyRunner.java:78)
    at java.base/java.lang.Thread.run(Thread.java:835)
java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 10
    at com.multi.thread.waitandnotify.WaitNotifyRunner$Producer.produce(WaitNotifyRunner.java:55)
    at com.multi.thread.waitandnotify.WaitNotifyRunner.lambda[=11=](WaitNotifyRunner.java:73)
    at java.base/java.lang.Thread.run(Thread.java:835)
Data in Buffer 0

你的缓冲区长度为 10

buffer = new int[10];

所以很明显,只要你的计数变量超过数组容量 10

    private volatile static Integer count;

你会得到 ArrayIndexOutOfBoundsException

你正在生产更多,消耗更少,消耗更少并且 produce/consume 的执行不是顺序的,它更像是循环法。

问题在于:

  • 对于 "full" 和 "empty" 状态,您只有一个共享的等待条件
  • 您是 运行 两个消费者线程("consumerWorker" 和 "delayedConsumer")和一个生产者线程("producerWorker")
  • 你没有像你应该的那样在 while 循环中重新检查条件

可能发生的事情是:

  1. 两个消费者线程都看到缓冲区是空的
  2. 生产者线程将一项放入缓冲区,并通知。
  3. 单个消费者线程唤醒并处理一项。缓冲区现在是空的。然后通知。
  4. 第二个消费者线程唤醒(而不是像您预期的生产者),不检查缓冲区是否为空,并尝试访问索引 -1 处的缓冲区。

您需要在从 wait 通话中醒来后重新检查情况。将 if 更改为 while。以下是如何为消费者做到这一点;您需要为制作人做同样的事情。

    void consume() {
        synchronized (key) {
            while (isEmpty()) {
                try {
                    key.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            buffer[--count] = 0;
            key.notify();
        }
    }

这样消费者就不会被来自其他消费者线程的 notify 搞糊涂了。

这也是 "recommended" the documentation of Object.wait:

The recommended approach to waiting is to check the condition being awaited in a while loop around the call to wait, as shown in the example below. Among other things, this approach avoids problems that can be caused by spurious wakeups.

(我引用了 "recommended" 因为它需要这样做才能正确实现它,因为 wait 也可以在不调用 notify 的情况下虚假地唤醒)