In-depth understanding of the fail-fast mechanism of ArrayList source code analysis

Overview

Earlier, we have learned about ArrayList. Next, we take ArrayList as an example to understand the fail-fast mechanism of Iterator.

1 Introduction to fail-fast

The fail-fast mechanism is an error mechanism in the Java Collection. A fail-fast event may occur when multiple threads operate on the contents of the same collection. For example: when a thread A traverses a collection through an iterator, if the content of the collection is changed by other threads; then when thread A accesses the collection, a ConcurrentModificationException will be thrown and a fail-fast event will be generated.

Before introducing the principle of fail-fast mechanism in detail, let's understand fail-fast through an example.

2 Fail-fast examples

Sample code: (FastFailTest.java)

import java.util.*;
import java.util.concurrent.*;

/*
 * @desc java集合中Fast-Fail的测试程序。
 *
 *   fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
 *   fast-fail解决办法:通过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。
 *
 *   本例中,分别测试ArrayList和CopyOnWriteArrayList这两种情况。ArrayList会产生fast-fail事件,而CopyOnWriteArrayList不会产生fast-fail事件。
 *   (01) 使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常;定义如下:
 *            private static List<String> list = new ArrayList<String>();
 *   (02) 使用时CopyOnWriteArrayList,不会产生fast-fail事件;定义如下:
 *            private static List<String> list = new CopyOnWriteArrayList<String>();
 *
 * @author skywang
 */
public class FastFailTest {

    private static List<String> list = new ArrayList<String>();
    //private static List<String> list = new CopyOnWriteArrayList<String>();
    public static void main(String[] args) {
    
        // 同时启动两个线程对list进行操作!
        new ThreadOne().start();
        new ThreadTwo().start();
    }

    private static void printAll() {
        System.out.println("");

        String value = null;
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
    }

    /**
     * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
     */
    private static class ThreadOne extends Thread {
        public void run() {
            int i = 0;
            while (i<6) {
                list.add(String.valueOf(i));
                printAll();
                i++;
            }
        }
    }

    /**
     * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
     */
    private static class ThreadTwo extends Thread {
        public void run() {
            int i = 10;
            while (i<16) {
                list.add(String.valueOf(i));
                printAll();
                i++;
            }
        }
    }

}

operation result:

Running the code throws an exception java.util.ConcurrentModificationException! That is, a fail-fast event is generated!

Result description:

(01) In FastFailTest, two threads are started at the same time to operate the list through new ThreadOne().start() and new ThreadTwo().start().

ThreadOne thread: Add 0,1,2,3,4,5 to the list in turn. After each number is added, the entire list is traversed through printAll().

ThreadTwo thread: Add 10, 11, 12, 13, 14, 15 to the list in sequence. After each number is added, the entire list is traversed through printAll().

(02) When a thread traverses the list, the content of the list is changed by another thread; a ConcurrentModificationException exception will be thrown, and a fail-fast event will be generated.

3 fail-fast solutions

The fail-fast mechanism is an error detection mechanism. It can only be used to detect errors, because the JDK does not guarantee that the fail-fast mechanism will happen. If you use a collection of fail-fast mechanisms in a multi-threaded environment, it is recommended to use "classes under the java.util.concurrent package" instead of "classes under the java.util package". Therefore, in this example, you only need to replace the ArrayList with the corresponding class in the java.util.concurrent package. That is, put the code

private static List<String> list = new ArrayList<String>();

replace with

private static List<String> list = new CopyOnWriteArrayList<String>();

This solution can be solved.

4 fail-fast principle

The fail-fast event is generated, which is triggered by throwing a ConcurrentModificationException exception. So, how does ArrayList throw ConcurrentModificationException?

We know that ConcurrentModificationException is an exception thrown when operating Iterator. Let's first look at the source code of Iterator. The Iterator of ArrayList is implemented in the parent class AbstractList.java. code show as below:

package java.util;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

    ...

    // AbstractList中唯一的属性
    // 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
    protected transient int modCount = 0;

    // 返回List对应迭代器。实际上,是返回Itr对象。
    public Iterator<E> iterator() {
        return new Itr();
    }

    // Itr是Iterator(迭代器)的实现类
    private class Itr implements Iterator<E> {
        int cursor = 0;

        int lastRet = -1;

        // 修改数的记录值。
        // 每次新建Itr()对象时,都会保存新建该对象时对应的modCount;
        // 以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等;
        // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
        int expectedModCount = modCount;

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

        public E next() {
            // 获取下一个元素之前,都会判断“新建Itr对象时保存的modCount”和“当前的modCount”是否相等;
            // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
            checkForComodification();
            try {
                E next = get(cursor);
                lastRet = cursor++;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet == -1)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

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

    ...
}

From it, we can find that when calling next() and remove(), checkForCommodification() is executed. If " modCount is not equal to expectedModCount ", ConcurrentModificationException will be thrown and fail-fast event will be generated.

To understand the fail-fast mechanism, we need to understand when "modCount is not equal to expectedModCount"! From the Itr class, we know that expectedModCount is assigned as modCount when the Itr object is created. Through Itr, we know: expectedModCount cannot be modified to not equal modCount. Therefore, what needs to be verified is when modCount will be modified.

Next, we look at the source code of ArrayList to see how modCount is modified.

package java.util;

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    ...

    // list中容量变化时,对应的同步函数
    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }


    // 添加元素到队列最后
    public boolean add(E e) {
        // 修改modCount
        ensureCapacity(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }


    // 添加元素到指定的位置
    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(
            "Index: "+index+", Size: "+size);

        // 修改modCount
        ensureCapacity(size+1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
             size - index);
        elementData[index] = element;
        size++;
    }

    // 添加集合
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        // 修改modCount
        ensureCapacity(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
   

    // 删除指定位置的元素 
    public E remove(int index) {
        RangeCheck(index);

        // 修改modCount
        modCount++;
        E oldValue = (E) elementData[index];

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // Let gc do its work

        return oldValue;
    }


    // 快速删除指定位置的元素 
    private void fastRemove(int index) {

        // 修改modCount
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }

    // 清空集合
    public void clear() {
        // 修改modCount
        modCount++;

        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

    ...
}

From this, we found that: whether it is add(), remove(), or clear(), as long as it involves modifying the number of elements in the collection, the value of modCount will be changed.

Next, we will systematically sort out how fail-fast is generated. Proceed as follows:

(01) Create a new ArrayList named arrayList.

(02) Add content to arrayList.

(03) Create a new " thread a ", and repeatedly read the value of arrayList through Iterator in "thread a" . (04) Create a new " thread b" and delete a "node A" in the arrayList in "thread b" .

(05) At this time, interesting events will occur. At some point, "thread a" creates an Iterator of arrayList. At this point "node A" still exists in the arrayList, when the arrayList is created, expectedModCount = modCount (assuming their value at this time is N) . At some point during "thread a" traversing the arrayList, "thread b" executes, and "thread b" deletes "node A" in the arrayList. When "thread b" executes remove() for deletion, it executes "modCount++" in remove(), and modCount becomes N+1 at this time ! "Thread a" then traverses, and when it executes to the next() function, it calls checkForCommodification() to compare the sizes of "expectedModCount" and "modCount"; and "expectedModCount=N", "modCount=N+1", so that Throws a ConcurrentModificationException exception and generates a fail-fast event.

At this point, we fully understand how fail-fast is generated ! That is, when multiple threads operate on the same collection, when a thread accesses the collection, the content of the collection is changed by other threads (that is, other threads change the value of modCount through methods such as add, remove, clear, etc.) ); at this time, a ConcurrentModificationException exception will be thrown, resulting in a fail-fast event.

5 The principle of solving fail-fast

In the above, the "measures to solve the fail-fast mechanism" are explained, and the "root cause of fail-fast" is also known. Next, let's talk more about how the fail-fast event is resolved in the java.util.concurrent package. It is still explained with CopyOnWriteArrayList corresponding to ArrayList. Let's first look at the source code of CopyOnWriteArrayList:

package java.util.concurrent;
import java.util.*;
import java.util.concurrent.locks.*;
import sun.misc.Unsafe;

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    ...

    // 返回集合对应的迭代器
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    ...
   
    private static class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot;

        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            // 新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。
            // 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }
    }
  
    ...

}

From this, we can see that:

(01) Unlike ArrayList, which inherits from AbstractList, CopyOnWriteArrayList does not inherit from AbstractList, it just implements the List interface.

(02) The Iterator returned by the iterator() function of ArrayList is implemented in AbstractList; and CopyOnWriteArrayList implements Iterator by itself.

(03) When next() is called in the Iterator implementation class of ArrayList, it will "call checkForComodification() to compare the size of 'expectedModCount' and 'modCount'"; however, in the Iterator implementation class of CopyOnWriteArrayList, there is no so-called checkForCommodification(), more ConcurrentModificationException will not be thrown!

If there is something inappropriate in the article, please correct me. You can also follow my WeChat public account: 好好学java, get high-quality learning resources, or join the QQ technical exchange group: 766946816, let's talk about java.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325957505&siteId=291194637