Fail fast and fail safe mechanisms of Iterator

Keywords: Java Container

Fail fast and fail safe mechanisms of Iterator

The difference between fail fast and fail safe: fail safe allows you to modify the data in the container during traversal, while fail fast does not.

Fail fast

Directly traverse the container. In the process of traversal, once it is found that the data in the container has been modified, it will immediately throw a ConcurrentModificationException, resulting in traversal failure.

The collection classes under the java.util package are fast failure mechanisms. The common containers traversed by fail fast are HashMap and ArrayList.

When an iterator is used to traverse a collection object, such as an enhanced for loop, if the contents of the collection object are modified (added, deleted or modified) during traversal, a ConcurrentModificationException will be thrown

Scene of fail fast

  1. Fail fast in single thread environment:
  • Example of fail fast in ArrayList:
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    for (int i = 0 ; i < 10 ; i++ ) {
        list.add(i + "");
    }
    Iterator<String> iterator = list.iterator();
    int i = 0 ;
    while(iterator.hasNext()) {
        if (i == 3) {
             list.remove(3);
        }
        System.out.println(iterator.next());
        i ++;
    }
} 

This code defines an ArrayList set and uses iterators to iterate. During the traversal process, deliberately remove an element in a certain iteration. At this time, fail fast will occur.

  • Fail fast in HashMap:
public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    for (int i = 0 ; i < 10 ; i ++ ) {
        map.put(i+"", i+"");
    }
    Iterator<Entry<String, String>> it = map.entrySet().iterator();
    int i = 0;
    while (it.hasNext()) {
       if (i == 3) {
           map.remove(3+"");
       }
       Entry<String, String> entry = it.next();
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
          i++;
    }
}

This code defines a hashmap object and stores 10 key value pairs. In the iterative traversal process, an element is removed using the remove method of map, resulting in a concurrenctmodificationexception thrown:

  1. In a multithreaded environment:
public class FailFastTest {
     public static List<String> list = new ArrayList<>();

     private static class MyThread1 extends Thread {
           @Override
           public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()) {
                     String s = iterator.next();
                     System.out.println(this.getName() + ":" + s);
                     try {
                       Thread.sleep(1000);
                     } catch (InterruptedException e) {
                        e.printStackTrace();
                     }
                }
                super.run();
           }
     }
     
     private static class MyThread2 extends Thread {
           int i = 0;
           @Override
           public void run() {
                while (i < 10) {
                     System.out.println("thread2:" + i);
                     if (i == 2) {
                         list.remove(i);
                     }
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     i ++;
                }
           }
     }
     
     public static void main(String[] args) {
           for(int i = 0 ; i < 10;i++){
               list.add(i+"");
           }
           MyThread1 thread1 = new MyThread1();
           MyThread2 thread2 = new MyThread2();
           thread1.setName("thread1");
           thread2.setName("thread2");
           thread1.start();
           thread2.start();
     }
}

Start two threads, one of which iterates over the list, and the other remove s an element during the iteration of thread 1. As a result, java.util.ConcurrentModificationException is thrown

Principle of fail fast

How does fail fast throw a ConcurrentModificationException and under what circumstances will it be thrown?

We know that for collections such as List and Map classes, we can traverse through iterators, and Iterator is actually just an interface. The specific implementation depends on the internal classes in the specific collection classes to implement Iterator and related methods. Here we take the ArrayList class as an example.

In ArrayList, when calling list.iterator(), its source code is:

public Iterator<E> iterator() {
        return new Itr();
}

That is, it will return a new Itr class, which is the internal class of ArrayList and implements the Iterator interface. The following is the source code of this class:

    /**
     * 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;
     
        public boolean hasNext() {
            return cursor != size;
        }
     
        @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];
        }
     
        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();
            }
        }
     
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
     
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

There are three attributes:

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

cursor refers to the index of the element to be traversed during the traversal of the collection
lastRet is cursor -1, which is - 1 by default, that is, when there is no previous one, it is - 1. It is mainly used to record the index of the element just traversed.
expectedModCount is the key variable for fail fast judgment. Its initial value is modCount in ArrayList. (modCount is a variable in the abstract class AbstractList, which defaults to 0. ArrayList inherits AbstractList, so it also has this variable. modCount is used to record the number of modifications made during collection operation. It is different from size and not necessarily equal to size.)
Let's look at it step by step:

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

The sign of the end of iterator iteration is that hasNext() returns false, and this method compares cursor with size (the number of elements in the collection). When cursor equals size, it indicates that the traversal has been completed.
Next, let's take a look at the next() method that we are most concerned about and see why fail fast occurs when a thread changes the collection structure during the iteration:

@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];
}

From the source code, every time you call the next() method, you will call the checkForComodification method before actually accessing the element. The source code of this method is as follows:

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

It can be seen that this method is the key to judge whether to throw a ConcurrentModificationException.
In this code, when modcount= This exception is thrown when expectedmodcount.
However, at the beginning, the initial value of expectedModCount is equal to modcount by default. Why does modcount appear= expectedModCount. Obviously, expectedModCount has not changed in the whole iteration process except the initial value modcount is given at the beginning, so only modcount may change. In the previous analysis on the capacity expansion mechanism of ArrayList, it can be seen that when ArrayList performs add, remove, clear and other operations involving modifying the number of elements in the set, ModCount will change (modCount++), so when another thread (concurrent modification) or the same thread traverses, calling the relevant method to change the number of sets will change the modCount so that the ConcurrentModificationException exception will be thrown in the checkForComodification method.
Similarly, the same principle occurs in HashMap.

Ways to avoid fail fast

After understanding the generation principle of fail fast mechanism, let's see how to solve fail fast

  • Method 1: during single thread traversal, if you want to remove, you can call the remove method of the iterator instead of the remove method of the collection class. Look at the source code of the remove method of the iterator in ArrayList:
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();
   }
}

It can be seen that the remove method will not modify the value of modCount and will not affect the subsequent traversal. Because the method remove cannot specify an element, only the currently traversed element can be removed. Therefore, calling this method will not cause fail fast. This method has limitations.

example:

public static void main(String[] args) {
   List<String> list = new ArrayList<>();
   for (int i = 0 ; i < 10 ; i++ ) {
       list.add(i + "");
   }
   Iterator<String> iterator = list.iterator();
   int i = 0 ;
   while(iterator.hasNext()) {
       if (i == 3) {
           iterator.remove(); //remove() method of iterator
       }
       System.out.println(iterator.next());
       i ++;
   }
}
  • Method 2: use the fail safe mechanism, use the copyonwriteraraylist class in java parallel contracting (java.util.concurrent) to replace ArrayList, and use ConcurrentHashMap to replace HashMap.

Fail safe

Fail safe: This traversal is based on a clone of the container.

Therefore, modifications to the contents of the container do not affect traversal. All containers under the java.util.concurrent package fail safely and can be used and modified concurrently under multithreading. Common containers traversed by fail safe include ConcerrentHashMap and CopyOnWriteArrayList.

Principle: the collection container using the security failure mechanism is not directly accessed on the collection content during traversal, but copies the original collection content first and traverses the copied collection. Because the copy of the original set is traversed during iteration, the changes made to the original set during traversal cannot be detected by the iterator, so the ConcurrentModificationException will not be triggered.

Disadvantages: the advantage of copying content is to avoid the ConcurrentModificationException, but similarly, the iterator cannot access the modified content, that is, the iterator traverses the set copy obtained at the moment when it starts traversing, and the iterator does not know the modification of the original set during traversal.

Posted by davestewart on Sat, 06 Nov 2021 11:56:03 -0700