java Concurrent Programming (20) - - (JUC Collection) Introduction to CopyOnWriteArrayList

Keywords: snapshot Java

This section begins with a formal introduction to JUC collection classes. We introduce them in the order of List, Set, Map and Queue. In this section, let's take a look at CopyOnWriteArray List.

Introduction to CopyOnWriteArrayList

CopyOnWriteArrayList is a thread-safe variant of ArrayList, where all variable operations (add, set, etc.) are implemented by a new copy of the underlying array.

Unlike ArrayList, it's about copying arrays and locking them.

CopyOnWriteArrayList, as its name implies, is an ArrayList that is copied at the time of writing. It means that when modifying the elements of a container, it does not modify directly on the original array, but copies an array first, then modifies it on the copied array, and assigns its reference value to the reference of the original array after modification. This reflects the separation of reading and writing, so that we can read the container at any time.

Source Code Analysis of CopyOnWriteArrayList

Let's look at the class declaration section of CopyOnWriteArrayList:

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

    /** The lock protecting all mutators */
    transient final ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private volatile transient Object[] array;

    }

It implements the List interface, so it implements Collection. In addition, we see that there are two class member variables, lock and array.
In the later source code analysis, we can see that CopyOnWriteArrayList is a thread-safe List implemented using dynamic array manipulation mechanism.

The so-called dynamic array operation mechanism is that the CRUD operation of arrays is carried out by volatile modified Object type arrays. When adding, setting, removing and other variable operations are performed, an array is created to assign the updated values to the array, and then passed to the array array array above to maintain the visibility of the operation. This is also the origin of the name CopyOnWriteArrayList. This generally requires a lot of overhead, but when the number of traversal operations greatly exceeds the number of variable operations, that is, the efficiency of reading operations is much higher than that of writing or modifying operations, which may be more effective than other alternatives.

CopyOnWriteArrayList's thread security implementation: We can see that it is achieved through a global Lock and volatile modified array. In addition, remove, set and other variable operations, we can always guarantee the memory visibility of the variable by assigning it to the array. Other threads can always read the latest array variable every time. In addition, when add,remove,set and other variable operations are carried out, exclusive locks will be added at the beginning of the operation and release locks at the end of the operation to ensure the security of the operation.

Let's take a look at some of his source code from the above analysis.

public void add(int index, E element) {
       final ReentrantLock lock = this.lock;  //Lock up
       lock.lock();
       try {
           Object[] elements = getArray();
           int len = elements.length;
           if (index > len || index < 0)
               throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);
           Object[] newElements;
           int numMoved = len - index;
           if (numMoved == 0)
        //If it is added at the last position, assign the array to one and add a new length.
               newElements = Arrays.copyOf(elements, len + 1);
           else {
        //Otherwise, create a new array, then copy the elements other than the deleted elements in the volatile array to the new array, and finally assign the new array to the volatile array.
               newElements = new Object[len + 1];
               System.arraycopy(elements, 0, newElements, 0, index);
               System.arraycopy(elements, index, newElements, index + 1,numMoved);
           }
           newElements[index] = element;
           setArray(newElements); //Copy
       } finally {
           lock.unlock();
       }
   }


public E set(int index, E element) {
       final ReentrantLock lock = l.lock;//Lock up
       lock.lock();
       try {
           rangeCheck(index);
           checkForComodification();
           E x = l.set(index+offset, element);
           expectedArray = l.getArray();//Copy
           return x;
       } finally {
           lock.unlock();
       }
   }

public E remove(int index) {
       final ReentrantLock lock = this.lock; //Lock up
       lock.lock();
       try {
           Object[] elements = getArray();
           int len = elements.length;
           E oldValue = get(elements, index);
           int numMoved = len - index - 1;
        // If the last element is deleted, it is processed directly through Arrays.copyOf(), without the need to create a new array.
           if (numMoved == 0)
               setArray(Arrays.copyOf(elements, len - 1));
        // Otherwise, create a new array, then copy the elements other than the deleted elements in the volatile array to the new array, and finally assign the new array to the volatile array.
           else {
               Object[] newElements = new Object[len - 1];
               System.arraycopy(elements, 0, newElements, 0, index);
               System.arraycopy(elements, index + 1, newElements, index,numMoved);
               setArray(newElements); //Copy
           }
           return oldValue;
       } finally {
           lock.unlock();
       }
   }

From the source code section above, we can see that CopyOnWriteArrayList does two more things than ArrayList in modifying the original array:

1. Locking: Make sure that when I modify the array, no one else can modify it.

2. Copy arrays: Either way, you need to copy arrays.

The above two things ensure that CopyOnWriteArrayList can handle itself in a multithreaded environment.

Let's look at his iterator implementation again:

public Iterator<E> iterator() {
   return new COWIterator<E>(getArray(), 0);
}

We see that the class COWIterator is called in the iterator. Let's take a look at its source code.

private static class COWIterator<E> implements ListIterator<E> {
   /** Snapshot of the array */
     private final Object[] snapshot;
     /** Index of element to be returned by subsequent call to next.  */
     private int cursor;

     private COWIterator(Object[] elements, int initialCursor) {
         cursor = initialCursor;
         snapshot = elements;
     }

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

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

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

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

     public int nextIndex() {
         return cursor;
     }

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

     //remove method is not supported
     public void remove() {
         throw new UnsupportedOperationException();
     }

    //set method is not supported
     public void set(E e) {
         throw new UnsupportedOperationException();
     }

     //The add method is not supported
     public void add(E e) {
         throw new UnsupportedOperationException();
     }
 }

We can see that COWSubListIterator does not support operations to modify elements. For example, COWSubListIterator throws exceptions for remove(),set(),add(), etc.

The iterator of CopyOnWriteArrayList does not fail quickly, that is, it does not throw a Concurrent ModificationException exception. This is because when he modifies it, it is for the copy array, and it has no effect on the original array. We can see that there is no lock mechanism in the iterator, so only read is provided, not add, modify and delete (throw Unsupported OperationException).

CopyOnWriteArrayList usage example

Above, we specifically analyze the thread security mechanism and implementation mechanism of CopyOnWriteArrayList, and then we will make a corresponding explanation on its use.

public class TestCopyOnWriteArrayList {
    // Fixme: When a list is an ArrayList object, the program will make an error.
    private static List<String> list = new ArrayList<String>();
    /*private static List<String> list = new CopyOnWriteArrayList<String>();*/
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(20);
        for(int i=0;i<100;i++){
            executor.execute(new TestList("aa"));
        }
    }

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

    private static class TestList extends Thread {
        TestList(String name) {
            super(name);
        }
        @Override
        public void run() {
            String val = Thread.currentThread().getName();
            list.add(val);
            printAll();
        }
    }
}

Running the program, when the list is an ArrayList object, the program will make an error and report an exception of type java.util.ConcurrentModificationException; when using the CopyOnWriteArrayList object, the program can complete iterator traversal operation.

Posted by Angry Lettuce on Wed, 03 Apr 2019 20:12:31 -0700