ArrayList генерирует исключение ConcurrentModificationException при попытке запустить метод .size()

Обновлять

Как заметил Иржи Тусек, ошибка в моем коде ввела в заблуждение многих любителей (и опытных) Java-разработчиков. Вопреки тому, что подразумевает название, ConcurrentModificationException имеет ли нет какое-либо отношение к многопоточности. Рассмотрим следующий код:

import java.util.List;
import java.util.ArrayList;

class Main {
  public static void main(String[] args) {
    List<String> originalArray = new ArrayList<>();
    originalArray.add("foo");
    originalArray.add("bar");
    List<String> arraySlice = originalArray.subList(0, 1);
    originalArray.remove(0);
    System.out.println(Integer.toString(arraySlice.size()));
  }
}

Это вызовет ConcurrentModificationException, несмотря на то, что потоки не задействованы.

Вводящее в заблуждение имя исключения навело меня на мысль, что моя проблема связана с тем, как я справлялся с многопоточностью. Я обновил заголовок своего поста, указав проблему действительный.

Оригинал (название: Как сообщить Java, что вы закончили изменять ArrayList в потоке?)

У меня есть код, который выглядит примерно так:

class MessageQueue {
    private List<String> messages = new ArrayList<>();
    private List<String> messagesInFlight = new ArrayList<>();

    public void add(String message) {
        messages.add(message);
    }

    public void send() {
        if (messagesInFlight.size() > 0) {
            // Wait for previous request to finish
            return;
        }

        messagesInFlight = messages.subList(0, Math.min(messages.size, 10));
        for( int i = 0; i < messagesInFlight.size(); i++ )
        {
            messages.remove(0);
        }

        sendViaHTTP(messagesInFlight, new Callback() {
            @Override
            public void run() {
                messagesInFlight.clear();
            }
        });
    }
}

Это используется в моем коде для целей аналитики. Каждые 10 секунд я звоню messageQueue.send() с таймера, и всякий раз, когда происходит интересное событие, я звоню messageQueue.add(). Этот класс работает *по большей части* -- я могу добавлять сообщения, и они отправляются через HTTP, и когда HTTP-запрос завершается, запускается обратный вызов

Проблема заключается во втором тике таймера. Когда я нажимаю на строку if (messagesInFlight.size() > 0) {, я получаю следующую ошибку:

java.util.ConcurrentModificationException
        at java.util.ArrayList$SubList.size(ArrayList.java:1057)

Кажется, я не могу прочитать .size() массива в одном потоке (обратный вызов второго таймера), потому что он думает, что массив все еще изменяется другим потоком (обратный вызов первого таймера). Однако я ожидаю, что поток первого таймера будет уничтожен и очищен после моего вызова sendViaHTTP, поскольку для его выполнения не было дополнительного кода. Кроме того, HTTP-запрос выполняется в течение 500 миллисекунд, поэтому в течение полных 9,5 секунд ничего не касается пустого массива messagesInFlight.

Есть ли способ сказать: «Эй, я закончил модифицировать этот массив, теперь люди могут безопасно его читать»? Или, возможно, лучший способ организовать мой код?

Я бы предложил использовать CopyOnWriteArrayList или другую подходящую параллельную коллекцию. Использование простого ArrayList в нескольких потоках вызывает проблемы.

Mark Rotteveel 22.05.2019 17:13

окружите места, где вы изменяете массив или вызываете размер синхронизированным блоком: synchronized(array) {

ControlAltDel 22.05.2019 17:14

@ControlAltDel Это хрупкое решение, и его действительно можно использовать только тогда, когда вы используете результат Collections.synchronizedList.

Mark Rotteveel 22.05.2019 17:15

Вам нужен учебник по многопоточности и параллелизму.

Raedwald 22.05.2019 17:16

@MarkRotteveel Я заменил messages на CopyOnWriteArrayList и получил ту же ошибку, но на этот раз в at java.util.concurrent.CopyOnWriteArrayList$COWSubList.checkFo‌​rComodification(Copy‌​OnWriteArrayList.jav‌​a:1207)

stevendesu 22.05.2019 17:17

@MarkRotteveel, хотя я согласен с тем, что разбрызгивание «синхронизированных» ключевых слов вокруг кода реализации несколько хрупко, почему использование синхронизированных блоков работает только с синхронизированным списком?

Frank Hopkins 22.05.2019 17:18

@Raedwald Точно, но бесполезно. Если есть какая-то ключевая концепция, которую я упускаю, не могли бы вы хотя бы назвать ее, чтобы я мог ее найти?

stevendesu 22.05.2019 17:18

Могу я спросить, как кто-то проголосовал за это «слишком широко»? Я буквально опубликовал точную выдаваемую ошибку, точный код, вызвавший ее, и точно описал, что я хочу выполнить. В этом нет ничего широкого. Я не говорил "что такое многопоточность и как мне это сделать?" Я сказал: «Как мне заставить этот код делать это без этой ошибки?»

stevendesu 22.05.2019 17:19

@stevendesu «слишком широкий» может исходить из ощущения, что лучшим решением будет другой дизайн всего этого плюс вводный курс по обработке параллелизма ...

Frank Hopkins 22.05.2019 17:20

@FrankHopkins При использовании synchronizedList атомарные (один метод) операции уже синхронизированы, поэтому вам нужно будет добавить synchronized-блоки только для неатомарных операций (вещей, которые требуют нескольких вызовов методов в списке). Это делает ваш код менее хрупким, потому что о необходимости явной синхронизации для атомарных операций довольно легко забыть, и это дает меньше беспорядка в вашем коде.

Mark Rotteveel 22.05.2019 17:23

@FrankHopkins Я почти уверен, что ответ шире, чем вопрос. Если есть определенный принцип дизайна, которым злоупотребляют или игнорируют, было бы полезно дать ему название. Что касается общего дизайна кода, я честно беру код C#, который уже был в нашей компании (используя Xamarin), и переписываю его на Java. Это точный макет и структура нашей существующей кодовой базы, массиву C# просто все равно, и он позволяет вам выполнять опасные параллельные акробатические трюки, предполагая, что вы знаете, что делаете.

stevendesu 22.05.2019 17:23

«Если есть ключевая концепция, которую я упускаю, не могли бы вы хотя бы назвать ее, чтобы я мог ее найти»: нет. Дело в том, что если вам нужен учебник, поиск нескольких «ключевых понятий» не поможет.

Raedwald 22.05.2019 17:24

@stevendesu Я не говорю, что это так, или это полностью мое мнение, просто то, что я предполагаю, является причиной этих «слишком широких» голосов.

Frank Hopkins 22.05.2019 17:31

@Raedwald Я не согласен на 100%. Вот буквально что такое вводный туториал *является*. Они учат ключевым понятиям.

stevendesu 22.05.2019 17:31

@MarkRotteveel IMO, есть смысл поощрять непосвященных (как автор этого вопроса) сначала попытаться найти «хрупкое» решение; это помогает учащемуся узнать, почему и где другие структуры данных или парадигмы программирования могут привести к лучшим решениям. Это определенно помогло мне, когда я изучал ООП: сначала мы кодировали хеш-таблицы и списки, используя процедурную парадигму... ах, какой беспорядок!

ControlAltDel 22.05.2019 17:32

@MarkRotteveel, но тогда это не «работает только с synchronizedList, просто меньше синхронизации шаблонов, когда вы комбинируете оба.

Frank Hopkins 22.05.2019 17:33

@FrankHopkins Я не сказал «работает только с synchronizedList», я сказал «только действительно пригодный для использования». Я не утверждал, что это не сработает, но я говорил об аспекте удобства использования (необходимость добавлять блоки synchronized слева и справа) и легкости забыть о них, что может привести к небольшим ошибкам.

Mark Rotteveel 22.05.2019 17:35

@MarkRotteveel ах, извините за неправильную цитату, не нашел ваш комментарий и подумал, что он пропал (но да, все еще там). И хорошо, я бы возразил, что, возможно, имеет смысл использовать больше синхронизированных блоков, а не полагаться в некоторых случаях на используемую структуру, а не на другие, что может затруднить правильное выполнение вещей, т.е. вам нужно сохранить два места, которые синхронизируются в уме. Плюс мне это звучало как "не будет работать".

Frank Hopkins 22.05.2019 17:38
Основы программирования на Java
Основы программирования на Java
Java - это высокоуровневый объектно-ориентированный язык программирования, основанный на классах.
Концепции JavaScript, которые вы должны знать как JS программист!
Концепции JavaScript, которые вы должны знать как JS программист!
JavaScript (Js) - это язык программирования, объединяющий HTML и CSS с одной из основных технологий Всемирной паутины. Более 97% веб-сайтов используют...
2
18
152
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Самая очевидная проблема, с которой вы столкнулись, заключается в том, что вы используете ArrayList.subList(), хотя, похоже, не понимаете, что он на самом деле делает:

Returns a view of the portion of this list ... The returned list is backed by this list.

В messagesInFlight вы храните Посмотреть, а не копировать. Когда вы удаляете первые сообщения из messages, вы фактически удаляете те самые сообщения, которые были в вашем messagesInFlight сразу после subList() звонка. Так что после цикла for будут совсем другие сообщения, а первые сообщения н будут полностью потеряны.


Что касается того, почему вы получаете ошибку, которую вы видите - subList() специально разрешает неструктурные изменения как подсписку, так и исходному списку (неструктурный означает замену элементов, а не их добавление или удаление), и пример в документации также демонстрирует, как исходный список может быть изменен путем изменения подсписка. Однако изменение исходного списка и последующий доступ к нему через подсписок не разрешены и могут привести к ConcurrentModifcationException, аналогично тому, что происходит, когда вы изменяете список, который вы перебираете с помощью итератора.

Обновление моего кода для устранения subList() (который я изначально использовал только для имитации формата нашего старого кода C# и не осознавал, что он не выполняет копирование) также фактически устранило проблему параллелизма. Я больше не получаю сообщение об ошибке, даже если снова использую ArrayList вместо CopyOnWriteArrayList! Я предполагаю, что каким-то образом создание представления отключило средство проверки параллелизма (несмотря на то, что оно вводило ошибку, так что хорошо, что ее поймали)

stevendesu 22.05.2019 17:28

@stevendesu На самом деле ConcurrentModificationException имеет отношение не к многопоточности, а к изменению списка и выполнению операции, которая ожидала, что список не будет изменен. Вы получили бы такое же исключение, если бы вызвали size() в подсписке в исходной теме.

Mark Rotteveel 22.05.2019 17:29

Смотрите мою правку. Это исключение на самом деле даже не относится к многопоточности, это защита от того, что вы модифицируете базовую структуру таким образом, что подсписок не может обнаружить и принять меры.

Jiri Tousek 22.05.2019 17:30

Ха красиво подмечено! И здесь все «наши эксперты» были введены в заблуждение исключением параллелизма и вскочили на поезд «узнайте о параллелизме, прежде чем использовать его», когда его нет. (Хотя, можно возразить, это похоже на сценарий, в котором обычно имеет смысл идти параллельно ^^) - и, очевидно, существует некоторый риск обратного вызова

Frank Hopkins 22.05.2019 17:40

@FrankHopkins Я видел, как имя этого исключения точно так же вводило в заблуждение многих опытных Java-программистов в моей карьере :) После быстрого просмотра кода я не заметил ничего плохого с точки зрения многопоточности (после исправления subList()), поэтому я нахожу флейм под постом слегка забавным.

Jiri Tousek 22.05.2019 17:44

Если вам нужно быть уверенным, что метод send не будет вызван, если предыдущий вызов не был завершен, вы можете удалить сообщения в коллекции fly и просто использовать переменную флага:

private AtomicBoolean inProgress = new AtomicBoolean(false);

public void send() {

    if(inProgress.getAndSet(true)) {
        return;
    }

    // Your logic here ...  

    sendViaHTTP(messagesInFlight, new Callback() {
        @Override
        public void run() {

            inProgress.set(false);
        }
    });

}

Я думаю, что это (ожидание завершения предыдущего вызова отправки) аннулируется проверкой сообщений в полете. Метод send не работает, если предыдущая отправка еще не завершена.

stevendesu 22.05.2019 17:29

@stevendesu спасибо, я обновляю свой ответ в соответствии с вашим комментарием

alexey28 22.05.2019 17:34

@ alexey28 alexey28 Все еще не эквивалентно тому, что делает исходный код - что, если придет третий вызов send(), в то время как первоначальный запрос еще не завершен?

Jiri Tousek 22.05.2019 17:38

Другие вопросы по теме