EventPublisher возникает java.util.ConcurrentModificationException при доступе к ArrayList

111
09 февраля 2021, 18:20

Есть класс:

@Component
public class EventPublisherImpl implements EventPublisher {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private List<EventListener> listeners = new ArrayList<>();
    @Override
    public void addListener(EventListener toAdd) {
        listeners.add(toAdd);
    }
    @Override
    public void removeListener(EventListener toRemove) {
        listeners.remove(toRemove);
    }
    public void publicEvent(AmiObject amiObject) {
        if (listeners != null && !listeners.isEmpty()) {
            Iterator<EventListener> iterator = listeners.iterator();
            while (iterator.hasNext())
                synchronized (iterator) {
                    EventListener eventListener = iterator.next();
                    if (eventListener != null) {
                        eventListener.publicEvent(amiObject);
                    }
                }
        }
    }
}

EventListener'ы добавляются в него из разных нитей.

Так вот при запуске возникает ошибка:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
    at ...EventPublisherImpl.publicEvent(EventPublisherImpl.java:34)
    at ...AmiObjectParserImpl.parseStr(AmiObjectParserImpl.java:45)
    at ...ConnectorImpl.listenSocket(ConnectorImpl.java:214)
    at ...ConnectorImpl.run(ConnectorImpl.java:92)
    at java.base/java.lang.Thread.run(Thread.java:834)

и указывает на строчку

EventListener eventListener = iterator.next();
Answer 1

Итератор ArrayList не поддерживает параллельное изменение списка кроме изменений через методы самого итератора. При обнаружении параллельного изменения выбрасывается ConcurrentModificationException об этом написано в документации:

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException.

К тому же сам ArrayList не является потокобезопасным, это может привести к другим ошибкам и расхождениям в данных. То, что список несинхронизирован и что нужно делать для безопасной работы из нескольких потоков тоже написано в документации:

Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be "wrapped" using the Collections.synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:

List list = Collections.synchronizedList(new ArrayList(...));

Чтобы исправить код нужно поступить согласно рекомендациям.

  1. Синхронизировать список через Collections.synchronizedList:
private List<EventListener> listeners = Collections.synchronizedList(new ArrayList<EventListener>());
  1. Перед чтением списка синхронизироваться по нему:
synchronized(listeners) {
    for(EventListener l : listeners) {
        l.publicEvent(amiObject);
    }
}
Answer 2

Захват такого монитора

synchronized (iterator) {...} 

не имеет смысла, т.к. listeners.iterator(); возвращает каждому зашедшему потоку новый объект - итератор, пруф из класса ArrayList

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
 *
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new Itr();
}

синхронизируйтесь на пример на самой коллекции

synchronized (listeners)  {...}
READ ALSO
Как вызвать Java метод из NDK(JNI)?

Как вызвать Java метод из NDK(JNI)?

В своем андроид приложении я использую NDK и у меня есть вот такой метод

96
Intellij Idea проблема с подсказками

Intellij Idea проблема с подсказками

Сделал pull проекта на другой компьютерТеперь Idea подчеркивает названия View в контроллере, которые я разрешаю TilesViewResolver, в jsp не видит модель...

123
Android retrofit копия запроса

Android retrofit копия запроса

В андроид(ява) приложение есть запрос (retrofit 2)

98
Как убрать метку @Deprecated с класса?

Как убрать метку @Deprecated с класса?

Пишу код в IIDEAЯ что-то такое нажал и метод Color теперь перечёркнутый

95