java.util.ConcurrentModificationException при итерации LinkedHashSet

Пожалуйста, помогите мне понять ошибку, которую я получаю:

private void replayHistory() {
    synchronized (alarmsHistory) {
        for (AlarmEvent alarmEvent : alarmsHistory) {
            LOG.error("replayHistory " + alarmEvent.type + " " + alarmEvent.source);
            sendNotification(alarmEvent.type, alarmEvent.source, alarmEvent.description, 
                    alarmEvent.clearOnOtherStations, alarmEvent.forceClearOnOtherStations);         
        }
    }
}

и метод, который добавляет к нему элемент

private void addToAlarmsHistory(AlarmEvent alarmEvent) {
    synchronized (alarmsHistory) {
        LOG.error("addToAlarmsHistory " + alarmEvent.type + " " + alarmEvent.source);
        alarmsHistory.add(alarmEvent);
    }
}

оба метода и набор

private volatile Set<AlarmEvent> alarmsHistory = new LinkedHashSet<AlarmEvent>();

определены в

JmxGwReloadThread extends Thread class

который является внутренним классом в

AlarmManager class

у которого есть метод

private void addToReplayHistory(AlarmEvent alarmEvent) {
    if ((jmxThread != null) && (jmxThread.isAlive())) {
        jmxThread.addToAlarmsHistory(alarmEvent);
    }
}

который вызывается разными интерфейсами (не могу гарантировать, когда и как часто)

В какой-то момент JmxThread запускается и вызывает метод replayHistory.

java.util.ConcurrentModificationException кидается, рут от

for (AlarmEvent alarmEvent : alarmsHistory) {

Код, вероятно, пытается добавить элемент в AlarmsHistory, и когда интератор

java.util.ConcurrentModificationException
    at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:390)
    at java.util.LinkedHashMap$KeyIterator.next(LinkedHashMap.java:401)
    at AlarmManager$JmxGwReloadThread.replayHistory(AlarmManager.java:568)
    at AlarmManager$JmxGwReloadThread.run(AlarmManager.java:532)

выдает исключение при вызове nextEntry, но разве синхронизация не должна предотвращать такую ​​проблему?

Логи показывают, что синхронизация не работает - replayHistory должна пройтись по всем своим элементам (могу заверить, что это более одного FM HEARTBEAT_INFO), но она прерывается вызовом addToReplayHistory.

2013-07-11 11:58:33,951 Thread-280 ERROR AlarmManager$JmxGwReloadThread.replayHistory(AlarmManager.java:570)  - replayHistory HEARTBEAT_INFO FM
2013-07-11 11:58:33,951 Thread-280 ERROR AlarmManager$JmxGwReloadThread.addToAlarmsHistory(AlarmManager.java:550)  -  addToAlarmsHistory HEARTBEAT_INFO FM
2013-07-11 11:58:33,952 Thread-280 ERROR Log4jConfigurator$UncaughtExceptionHandler.uncaughtException(Log4jConfigurator.java:253)  - Detected uncaught exception in thread: Thread-280

person Łukasz    schedule 11.07.2013    source источник
comment
Не могли бы вы сделать свой код SSCCE? Особенно покажите нам полный класс, который демонстрирует такое поведение. Потому что ваш текущий размещенный код должен работать до сих пор.   -  person Uwe Plonus    schedule 11.07.2013
comment
Как вы можете выполнить итерацию непосредственно по LinkedHashMap? alarmsHistory не может быть LinkedHashMap, так как for (AlarmEvent alarmEvent : alarmsHistory) { .. } не будет компилироваться. Ваш пример мне непонятен. Пожалуйста, разместите полный пример в качестве комментария перед предложением.   -  person mmirwaldt    schedule 11.07.2013
comment
Отредактировал мой первоначальный пост, извините за неполные и вводящие в заблуждение данные.   -  person Łukasz    schedule 11.07.2013
comment
В коде используется LinkedHashSet, но в трассировке стека упоминается LinkedHashMap?   -  person Dahaka    schedule 11.07.2013
comment
Также я думаю, что первый вызов LOG должен быть в цикле, а не перед ним.   -  person Dahaka    schedule 11.07.2013
comment
Да, это в лоппе, у меня были проблемы с идентификацией и я скопипастил не в ту строку.   -  person Łukasz    schedule 11.07.2013
comment
@mmirwaldt Я не повторяю LinkedHashMap, LinkedHashSet расширяет HashSet. Hashset имеет общедоступный Iterator‹E› iterator() { return map.keySet().iterator(); }, который извлекает итератор из реализации карты, поэтому он находится в трассировке стека.   -  person Łukasz    schedule 11.07.2013


Ответы (4)


Одна вещь, о которой должен знать OP (и, вероятно, большинство людей):

ConcurrentModificationException не имеет ничего общего с многопоточностью.

Хотя многопоточность делает это намного проще, но суть этой проблемы не имеет ничего общего с многопоточностью.

В основном это вызвано такими сценариями, как

  1. Получить итератор из коллекции,
  2. Прежде чем закончить использование этого итератора, коллекция структурно модифицируется.
  3. Продолжайте использовать итератор после 2. Итератор обнаружит, что коллекция структурно изменена, и выдаст исключение ConcurrentModificationException.

Конечно, не все коллекции имеют такое поведение, например. ConcurrentHashMap. Определение «структурно модифицированный» также отличается для разных коллекций.

Это означает, что даже у меня есть только 1 поток, если я сделаю что-то вроде:

    List<String> strings = new ArrayList<String>();
    //....
    for (String s: strings) {  // iterating through the collection
        strings.add("x");   // structurally modifying the collection
    }

Я получу ConcurrentModificationException, даже если все это происходит в одном потоке.

Существуют различные способы решения проблемы, в зависимости от вашего требования или проблемы. например

  1. Если это просто из-за многопоточного доступа, правильный синхронизирующий доступ может быть одним из решений.
  2. Вы можете использовать коллекцию, в которой итератор защищен от структурных изменений (например, ConcurrentHashMap).
  3. Настройте свою логику, чтобы либо повторно получить итератор снова, если вы изменили коллекцию, либо внести изменения с помощью итератора (некоторые реализации коллекции позволяют это), либо убедитесь, что модификация коллекции происходит после того, как вы закончите использовать итератор.

Учитывая, что ваш код, кажется, имеет правильную синхронизацию на alarmHistory, есть два направления, которые вы хотели бы проверить.

  1. Возможна ли модификация alarmHistory внутри sendNotification()? Например, добавление или удаление из alarmHistory?
  2. Есть ли другой возможный несинхронизированный доступ к alarmHistory, который может изменить структуру?
person Adrian Shum    schedule 12.07.2013

Если один поток повторяется, а другой добавляется, вы попали в шланг.

Учитывая, что ваш код похоже синхронизирует доступ к двум релевантным блокам кода, найдите другой несинхронизированный код, который добавляет/удаляет из AlarmsHistory.

person Bohemian♦    schedule 11.07.2013
comment
Если блокировка удерживается потоком, который выполняет итерацию, как можно добавить другой поток?? - person prasanth; 11.07.2013
comment
Но если у двух потоков есть свои экземпляры, почему там ConcurrentModificationException? - person Dahaka; 11.07.2013
comment
@Dahaka Я только что понял, что объект блокировки - это коллекция. Дальнейшие правки :/ - person Bohemian♦; 11.07.2013

Единственная мысль, которая приходит мне в голову, что у вас закулисная логика. Я думаю, что sendNotification каким-то образом рекурсивно вызывает addToReplayHistory. Таким образом, многопоточность — отвлекающий маневр, файл журнала показывает только один вовлеченный поток, и сразу же после sendNotification есть вызов addToReplayHistory, который изменяет коллекцию и ломает интератор.

Дополнительная информация находится в javadoc для исключения:

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

person kan    schedule 11.07.2013

Чтобы добавить некоторые детали к ответу Кана:

Блок synchronized в Java является реентерабельным:

Реентерабельная синхронизация

Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.

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

То, что вы должны искать, чтобы исправить исключение, - это рекурсивные вызовы между синхронизированными блоками или несинхронизированный доступ к alarmsHistory.

Вы также можете просматривать параллельные коллекции, такие как ConcurrentSkipList или CopyOnWriteArraySet. Оба должны предотвратить исключение, но остерегайтесь их поведения и характеристик производительности, описанных в JavaDoc.

person joe776    schedule 12.07.2013