Почему этот код вызывает исключение java ConcurrentModificationException?

public final class ClientGateway {

   private static ClientGateway instance;
   private static List<NetworkClientListener> listeners = Collections.synchronizedList(new ArrayList<NetworkClientListener>());
   private static final Object listenersMutex = new Object();
   protected EventHandler eventHandler;


   private ClientGateway() {
      eventHandler = new EventHandler();
   }

   public static synchronized ClientGateway getInstance() {
      if (instance == null)
         instance = new ClientGateway();
      return instance;
   }

   public void addNetworkListener(NetworkClientListener listener) {
     synchronized (listenersMutex) {
        listeners.add(listener);
     }
   }


   class EventHandler {

     public void onLogin(final boolean isAdviceGiver) {
        new Thread() {
           public void run() {
              synchronized (listenersMutex) {
                 for (NetworkClientListener nl : listeners) 
                    nl.onLogin(isAdviceGiver);
              }
           }
        }.start();
     }

   }
}

Этот код генерирует исключение ConcurrentModificationException. Но я подумал, что если они оба синхронизированы на listenersMutex, то они должны выполняться последовательно? Весь код в функциях, которые работают со списком слушателей, работает в синхронизированных блоках, которые синхронизируются на мьютексе. Единственным кодом, который изменяет список, являются addNetworkListener(...) и removeNetworkListener(...), но removeNetworkListener никогда не вызывается в данный момент.

Что, по-видимому, происходит с ошибкой, так это то, что NetworkClientListener все еще добавляется, пока функция/поток onLogin выполняет итерацию слушателей.

Спасибо за ваше понимание!

EDIT: NetworkClientListener является интерфейсом и оставляет реализацию onLogin кодировщику, реализующему функцию, но его реализация функции не имеет доступа к списку слушателей.

Кроме того, я только что полностью перепроверил, и нет никаких изменений списка вне функций addNetworkListener() и removeNetworkListener(), другие функции только перебирают список. Изменение кода с:

for (NetworkClientListener nl : listeners) 
   nl.onLogin(isAdviceGiver);

To:

for(int i = 0; i < listeners.size(); i++)
   nl.onLogin(isAdviceGiver);

По-видимому, решает проблему параллелизма, но я уже знал об этом и хотел бы знать, в первую очередь, что вызывает это.

Еще раз спасибо за вашу постоянную помощь!

Исключение: Исключение в потоке "Thread-5" java.util.ConcurrentModificationException в java.util.ArrayList$Itr.checkForComodification(ArrayList.java:782) в java.util.ArrayList$Itr.next(ArrayList.java:754) в chapchat.client.networkcommunication.ClientGateway$EventHandler$5.run(ClientGateway.java:283)

ИЗМЕНИТЬ Хорошо, я чувствую себя немного глупо. Но спасибо за всю вашу помощь! Особенно MJB & jprete!

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

Решение: предложение MJB использовать CopyOnWriteArrayList вместо синхронизированных списков.


person random dude    schedule 04.05.2011    source источник
comment
код NetworkClientListener#onLogin был бы полезен   -  person Affe    schedule 05.05.2011
comment
Где Комодификация? Stacktrace был бы полезен   -  person John Vint    schedule 05.05.2011
comment
На всякий случай, если вы замените nl.onLogin на system.out.println(nl); проблема все еще возникает?   -  person MJB    schedule 05.05.2011
comment
@MJB ну, это странно, я заменил его, и он работает. Я собираюсь просмотреть каждую реализацию onLogin и посмотреть, делает ли что-то что-то. Но что меня смущает, так это то, что каждая реализация полностью находится в другом пакете, как они могут изменять прослушиватели частного списка?   -  person random dude    schedule 05.05.2011
comment
Возможный дубликат ConcurrentModificationException, несмотря на использование synchronized   -  person Raedwald    schedule 28.03.2016


Ответы (3)


Мьютексы защищают только от доступа из нескольких потоков. Если nl.onLogin() имеет логику, которая добавляет прослушиватель в список listeners, то может быть выброшен ConcurrentModificationException, потому что к нему обращаются (итератором) и изменяют (добавлением) одновременно.

EDIT: дополнительная информация, вероятно, поможет. Насколько я помню, коллекции Java проверяют одновременные модификации, сохраняя счетчик модификаций для каждой коллекции. Каждый раз, когда вы выполняете операцию, которая изменяет коллекцию, счетчик увеличивается. Для проверки целостности операций проверяется счет в начале и конце операции; если счетчик изменился, то коллекция выдает ConcurrentModificationException в точке доступа, а не в точке модификации. Для итераторов он проверяет счетчик после каждого вызова next(), поэтому при следующей итерации цикла через listeners вы должны увидеть исключение.

person jprete    schedule 04.05.2011
comment
Я отредактировал основной пост, чтобы отметить, что nl.onLogin не реализован мной, однако его реализация не имеет доступа к списку слушателей. Но спасибо за информацию! - person random dude; 05.05.2011

Должен признаться, что я тоже этого не вижу - если действительно не вызывается removeListeners.

Какова логика бита nl.onLogin? Если бы он изменил что-то, это могло бы вызвать исключение.

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

person MJB    schedule 04.05.2011
comment
Спасибо за совет по CopyOnWriteArrayList! Я, вероятно, в конечном итоге использую это, поскольку слушатели редко добавляются/удаляются, однако я все еще пытаюсь понять, почему я получаю параллельное исключение в первую очередь. (Я отредактировал основной пост, чтобы отметить, что nl.onLogin не реализован мной, однако его реализация не имеет доступа к списку слушателей) - person random dude; 05.05.2011

Вместо ArrayList можно использовать потокобезопасный класс CopyOnWriteArrayList, который не генерирует ConcurrentModificationException, даже если он изменяется во время итерации. Во время итерации, если его попытаются изменить (добавить, обновить), он сделает копию списка, но итератор продолжит работу с исходным.

Это немного медленнее, чем ArrayList . Это полезно в тех случаях, когда вы не хотите синхронизировать итерации.

person Abhinav    schedule 05.05.2011