ConcurrentModificationException иногда выкидывается, а иногда нет

Заметил одну особенность.

    public static void main(String args[]){
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(20);
        list.add(30);
        list.add(40);
        list.add(50);
        for(int ints : list){
            if(ints == 30){
                list.remove(new Integer(ints));
            }
        }
    }

Как и ожидается, в ран-тайме выкидывается ConcurrentModificationException. (Т.к. насколько мне известно enchanced for под капотом работает на итераторе, как-то связано с полем snapshot) Но! Если мы немного изменим данный пример, то все прекрасно работает.

    public static void main(String args[]){
        List<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(20);
        list.add(30);
        for(int ints : list){
            if(ints == 20){
                list.remove(new Integer(ints));
            }
        }
    }

Сейчас всё прекрасно работает. Почему так происходит? В одном случае выкидывается исключение, а в другом нет! Проверяю на Java 8. Прошу дать исчерпывающий ответ)


Ответы (2 шт):

Автор решения: Jackkobec

Исчерпывающий ответ заключается в том, что так не нужно делать в принципе. Используйте list.removeIf

list.removeIf(item -> item == 20);
→ Ссылка
Автор решения: Alex Rudenko

Так происходит из-за того, что удаляется предпоследний элемент в списке.

Цикл for (int ints : list) неявно использует итератор списка и фактически эквивалентен следующему коду:

for (Iterator<Integer> i = ints.iterator(); i.hasNext();) {
    int ints = i.next().intValue(); // unboxing
    // ...
}

Этот итератор может быть реализован во внутреннем классе Itr, пример в JDK8

Метод remove(Object x) для ArrayList вообще НЕ использует итератор, а просто меняет размер списка size. Например, в JDK8 он реализован так

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

Соответственно метод итератора hasNext в случае удаления предпоследнего элемента вернёт false:

        public boolean hasNext() {
            return cursor != size;
        }

и метод next(), в котором, собственно вызывается проверка на одновременную модификацию, не будет вызван, так как цикл уже закончится.

При попытке удалить другие (не предпоследние) элементы методом List::remove во время итерации по списку, будет выброшено исключение, так как cursor != size == true.

По этому поводу неоднократно открывались баги в JDK:

  • JDK-8136821: ConcurrentModificationException not thrown when removing the next to last element, less than expected number of iterations
  • JDK-8210207: Concurrent Modification Exception not thrown when removing second last element from list

которые как правило закрывались так как выбрасывание такого рода исключения не гарантируется в fail-fast итераторах, о чём указано в документации API:

JavaDoc JDK 8 java.util.ArrayList:

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. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

→ Ссылка