Нечетное исключение ConcurrentModificationException

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

Я понимаю, что есть и другие ситуации, в которых может быть выдано это исключение. Из CME JavaDoc:

Обратите внимание, что это исключение не всегда указывает на то, что объект был одновременно изменен другим потоком. Если один поток выдает последовательность вызовов методов, которая нарушает контракт объекта, объект может выдать это исключение.

Вот мой тестовый код:

TestListener test = new TestListener();
Assert.assertTrue(evtMgr.register(test));
Assert.assertFalse(testBool);
evtMgr.fire(new QuestStartEvent(null));
Assert.assertTrue(testBool);

testBool = false;
evtMgr.unregister(test);
evtMgr.fire(new QuestStartEvent(null));
Assert.assertFalse(testBool);

EventManager выглядит так:

public boolean register(Listener listener) {
    return listeners.add(new ListenerHandle(listener));
}

public void unregister(Listener listener) {
    listeners.stream().filter((l) -> l.getListener() == listener)
            .forEach(listeners::remove);
}

public <T extends Event> T fire(T event) {
    listeners.forEach((listener) -> listener.handle(event));
    return event;
}

Где ConcurrentModificationException находится в .forEach(listeners::remove);

ListenerHandle выглядит так:

private final Listener listener;
private final Map<Class<? extends Event>, Set<MethodHandle>> eventHandlers;

public ListenerHandle(Listener listener) {
    this.listener = listener;
    this.eventHandlers = new HashMap<>();

    for (Method meth : listener.getClass().getDeclaredMethods()) {
        EventHandler eh = meth.getAnnotation(EventHandler.class);
        if (eh == null || meth.getParameterCount() != 1) {
            continue;
        }

        Class<?> parameter = meth.getParameterTypes()[0];
        if (!Event.class.isAssignableFrom(parameter)) {
            continue;
        }

        Class<? extends Event> evtClass = parameter.asSubclass(Event.class);
        MethodHandle handle = MethodHandles.lookup().unreflect(meth);
        Set<MethodHandle> handlers = eventHandlers.get(evtClass);
        if (handlers == null) {
            handlers = new HashSet<>();
            eventHandlers.put(evtClass, handlers);
        }

        handlers.add(handle);
    }
}

public void handle(Event event) {
    Class<? extends Event> clazz = event.getClass();
    Set<MethodHandle> handles = eventHandlers.get(clazz);
    if (handles == null || handles.isEmpty()) {
        return;
    }

    for (MethodHandle handle : handles) {
        handle.invoke(listener, event);
    }
}

(С try-catches вырезаны для удобочитаемости)

И трассировка стека:

java.util.ConcurrentModificationException
    at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1545)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at EventManager.unregister(EventManager.java:54)

(строка 54 — .forEach(listeners::remove);)


person DziNeIT    schedule 14.09.2014    source источник
comment
Вам нужно удалить итератор, а не список.   -  person xio4    schedule 14.09.2014
comment
@xio4 xio4 спасибо, у вас есть объяснение, почему? Я хотел бы узнать больше о проблеме, чтобы больше не сталкиваться с ней в будущем.   -  person DziNeIT    schedule 14.09.2014
comment
stackoverflow.com/questions/1196586 /   -  person xio4    schedule 14.09.2014
comment
@ xio4 а, конечно, имеет смысл - спасибо за помощь   -  person DziNeIT    schedule 14.09.2014


Ответы (1)


Вы получаете исключение параллельной модификации, потому что вы изменяете коллекцию одновременно с ее итерацией. Вместо

listeners.stream().filter((l) -> l.getListener() == listener)
         .forEach(listeners::remove);

вы должны либо использовать традиционную идиому, основанную на Iterator, и вызывать remove() для итератора, а не для коллекции, либо сначала выполнить итерацию, чтобы собрать набор объектов, которые вы хотите удалить, а затем удалить их за один раз после завершения начальной итерации:

listeners.removeAll(
   listeners.stream().filter((l) -> l.getListener() == listener)
            .collect(Collectors.toList()));

Подход с использованием итератора, вероятно, более эффективен.

person Ian Roberts    schedule 14.09.2014