A new understanding of the concurrency modificationexception and strange phenomena in Java collection iterating through the source code

Keywords: Java JDK

A new understanding of the concurrency modificationexception and strange phenomena in Java collection iterating through the source code

preface

We often encounter scenarios in projects that need to filter invalid data. The more common way is to move elements that do not meet the requirements out of the collection to get the results we want when iterating. But if you don't know the collection well, you may throw an exception or have some strange phenomena. Let's recognize it again today.
 

1, Scene settings

Suppose there is a set list of [11, 22, 22, 33]. Now the requirement is that we don't need 22 elements in it, but we don't know the order of the elements. How to operate?

 

2, Accident site

2.1 code throwing exception ConcurrentModificationException

If you don't say much, just go to the error sample code. We will analyze the cause of the error later

public static void test2For() {
    System.out.println("enhance for loop");
    List<String> list = new ArrayList<String>(Arrays.asList("11","22","22","33"));

    for (String i : list) {
        // Did something on behalf of print
        System.out.println("Current element:" + i);
        if ("22".equals(i)) {
            list.remove(i);
        }
    }
    System.out.println("The processed sets are:" + list);
}

Operation result:

2.2 no error is reported, but the expected result is not achieved

Or directly on the same error sample code:

public static void testfori() {
    System.out.println("for i loop");
    List<String> list = new ArrayList<String>(Arrays.asList("11", "22", "22", "33"));
    for (int i = 0; i < list.size(); i++) {
        // Did something on behalf of print
        System.out.println("Current element:" + list.get(i));
        if ("22".equals(list.get(i))) {
            list.remove(i);
        }
    }
    System.out.println("The processed sets are:" + list);
}

Run result: we will find that the second 22 is not deleted from the collection as expected. Why? Next section!

 

3, Source code analysis

3.1 why is ConcurrentModificationException thrown?

Let's look at the stack information of the run results in Section 2.1:

enhanceforloop
//Current element:11
//Current element:22
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
	at com.chengcheng.CirculateDemo.test2For(CirculateDemo.java:56)
	at com.chengcheng.CirculateDemo.main(CirculateDemo.java:12)

You can see that the exception is thrown in the checkForComodification() method of ArrayList$Itr. Maybe we are not familiar with this, but we can see that the next row is called checkForComodification() in next() method. Let's look at the source code of these two methods:

Before you look at the source code, you still need to add a sentence. I'm afraid that a few small whites with poor foundation will ask: how can there be the next() method? The code is clear. This is because our enhanced for loop is just a syntactic sugar. Its essence is actually an iterator. This enhanced for loop will be compiled into an iterator operation when the code is compiled. We can see what the compiled code looks like.

public static void test2For() {
    System.out.println("enhance for loop");
    List<String> list = new ArrayList(Arrays.asList("11", "22", "22", "33"));
    Iterator i$ = list.iterator();

    while(i$.hasNext()) {
        String i = (String)i$.next();
        System.out.println("Current element:" + i);
        if ("22".equals(i)) {
            list.remove(i);
        }
    }

    System.out.println("The processed sets are:" + list);
}

Well, back to the point, let's look at the source code of next() and checkforconformity()

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

The code is very clear. In the checkforconformity() method, a modCount and expectedModCount equal comparison operation is done. When they are not equal, a ConcurrentModificationException exception will be thrown.

What are these two parameters for?

First of all, take a look at the official explanation of modCount parameter in AbstractList (see below). It doesn't matter if you don't understand English. Roughly speaking, this parameter is used to record the number of times that the set size of the list has changed (modified). It will be used by the iterator iterator (i.e. the checkforverification method we saw earlier) to implement a fast failed iteration To prevent strange behavior (like our 2.2).

Attachment: official notes of JavaDoc

/**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

When will this parameter change? Of course, when the set size changes, such as the add and remove methods. Let's take the source code of the remove method and see a modCount + + operation. Similarly, we can see similar operations in the add method.

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    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

    return oldValue;
}

After looking at modCount, let's look at another parameter expectedModCount

/**
  * An optimized version of AbstractList.Itr
  */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
    // Other source codes are omitted below
}

We see that there is no official explanation for this parameter. Literally, it means the expected modification times of the collection, and it is equal to modCount when it is initialized. **So why check if the two parameters are equal? **In fact, I believe you may have the answer in mind. The 2.2 mentioned above is not the best example. The result of not checking is that the program fails to achieve the expected result, but it seems that there is no problem with the business logic, which is very confusing, right. Let's take a look at the official definition and introduction of ConcurrentModificationException.

3.2 what is concurrentmodificationexception?

The literal translation of concurrent modification exception is concurrent modification exception. The official explanation is as follows:

/**
 * This exception may be thrown by methods that have detected concurrent
 * modification of an object when such modification is not permissible.
 * <p>
 * For example, it is not generally permissible for one thread to modify a Collection
 * while another thread is iterating over it.  In general, the results of the
 * iteration are undefined under these circumstances.  Some Iterator
 * implementations (including those of all the general purpose collection implementations
 * provided by the JRE) may choose to throw this exception if this behavior is
 * detected.  Iterators that do this are known as <i>fail-fast</i> iterators,
 * as they fail quickly and cleanly, rather that risking arbitrary,
 * non-deterministic behavior at an undetermined time in the future.
 * <p>
 * Note that this exception does not always indicate that an object has
 * been concurrently modified by a <i>different</i> thread.  If a single
 * thread issues a sequence of method invocations that violates the
 * contract of an object, the object may throw this exception.  For
 * example, if a thread modifies a collection directly while it is
 * iterating over the collection with a fail-fast iterator, the iterator
 * will throw this exception.
 *
 * <p>Note that fail-fast behavior cannot be guaranteed as it is, generally
 * speaking, impossible to make any hard guarantees in the presence of
 * unsynchronized concurrent modification.  Fail-fast operations
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness: <i>{@code ConcurrentModificationException}
 * should be used only to detect bugs.</i>
 */

Don't worry about the students who are not good at English. Let's roughly say the meaning of the official paragraph: This is an exception thrown when a method detects that a specified object has concurrent modification, and such concurrent modification is not allowed. For example, when a thread is iterating over a collection, another thread is modifying it at the same time. Some of the iterators implemented by JDK do this check and throw exceptions to achieve a quick failure effect, so as to avoid an uncertain result.

Of course, the official also said later that this exception is not necessarily thrown because another thread has made concurrent changes to the object. When a single thread breaks the Convention of method call, it is also possible to report an error (for example, our example in 2.1 is the concurrent change of a single thread). Usually this exception is used to detect code bugs.

3.3 why did the 2.2 case fail to meet expectations?

In fact, the reason is very simple, because we get elements through their subscripts during traversal. However, when we use the remove method, the subscripts of elements in this list may change (why is it possible? Think about it), why has it changed? This is related to the implementation of ArrayList. ArraysList itself is implemented through arrays. Therefore, when an element is added or deleted, the internal array will be shifted. Look at the source code below and you will see a System.arraycopy(elementData, index+1, elementData, index,numMoved); This operation will make all the elements after the removed element move (move forward one bit) to change the subscript of the element. When we delete the first 22 element, the second 22 element will go to the previous 22 position. Then, in the next iteration, the subscript points to 33, so it is equivalent to missing the second 22 element.

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    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

    return oldValue;
}

 

4, Scenario solutions and feasibility reasons

4.1 delete in reverse order

public static void testforiReverse() {
    System.out.println("for i loop");
    List<String> list = new ArrayList<String>(Arrays.asList("11", "22", "22", "33"));
    for (int i = list.size() - 1 ; i > 0; i--) {
        // Did something on behalf of print
        System.out.println("Current element:" + list.get(i));
        if ("22".equals(list.get(i))) {
            list.remove(i);
        }
    }
    System.out.println("The processed sets are:" + list);
}

We won't see the running results. Why can we just delete them in reverse order? As we mentioned in 3.3, when the positive order is deleted, the list will make an arraycopy action to move all the elements after the deletion one bit forward, that is to say, the subscripts of all elements before the deletion will not change. So when we use reverse order to delete, the previous element subscript does not change, and we will get the correct result.

4.2 using pure iterator operation

public static void testIter() {
    System.out.println("iterator loop ");
    List<String> list = new ArrayList<String>(Arrays.asList("11", "22", "22", "33"));

    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        String ele = it.next();
        // Did something on behalf of print
        System.out.println("Current element:" + ele);
        if ("22".equals(ele)) {
            it.remove();
        }
    }
    System.out.println("The processed sets are:" + list);
}

Why can the remove method of iterator do it? The iterator's cursor parameter will be list.remove After that, it will change accordingly, so that the new subscripts of the following elements will also be synchronized to the iterator, so there will be no such strange phenomenon as 2.2 that the elements are missing and not handled. Then why not throw the exception? After the previous understanding, we know that exceptions will be thrown when modCount and expectedModCount are inconsistent, so let's take a look at the source code of the iterator's remove method. It also modifies these two parameters after remove to make them equal, so the removal of the iterator will not cause exceptions.

Source code attached:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

4.3 Java 8's stream API

public static void testStreamAPI(){
    System.out.println("Stream API");
    List<String> list = new ArrayList<String>(Arrays.asList("11","22","22","33"));
    list = list.stream().filter(ele -> !"22".equals(ele)).collect(Collectors.toList());
    System.out.println(list);
}

The Stream API java8 is really easy to use. It's easy to do things.

Well, here today, welcome to my official account "Bug hole", record the pits and learning process together, though I am a little updated owner, ha ha ha.
 

Posted by javier on Sat, 13 Jun 2020 19:40:08 -0700