CopyOnWriteArrayList Source Details

Keywords: snapshot Programming

1. Overview of CopyOnWriteArrayList

CopyOnWriteArrayList is a thread-safe collection that stores elements internally through an array. From the name, it can be seen that this is a List of "Write-Time Replication". Every time the List is modified, a new array will be created. copy the original array elements to the new array, and modify the new array. After the modification, update the original array to refer to the new array.

There is no need to lock when reading. The operation of reading is carried out on the original array. Multithread can read concurrently. But writing needs to be locked, otherwise multithreaded writing creates N arrays. Read operation is performed on the original array, write operation is performed on the new array, without interference, read and write separation.

Write-time replication can cause a lot of performance problems. When there are many write operations, each modification will copy a new array and the original array elements to the new array, which is inefficient. Therefore, CopyOnWriteArray List is suitable for reading more than writing scenarios.

Look at the CopyOnWriteArrayList class diagram:

CopyOnWriteArrayList implements List, Random Access, Cloneable, Serializable interfaces. Internally, elements are stored through arrays, so it is a "random access" List. Random Access is an empty interface that implements it only to indicate that CopyOnWriteArrayList is a "Random Access" List.

3. Construction Method

CopyOnWriteArrayList has three constructions. Look at the source code:

//Write operation locking
final transient ReentrantLock lock = new ReentrantLock();

//An array of internal storage elements
private transient volatile Object[] array;

//This method and the setArray method below are both package access privileges. User code cannot access this method, so it is impossible to modify the internal array directly.
//And both methods are final and cannot be overridden by subclasses, avoiding the modification of arrays by subclasses.
final Object[] getArray() {
    return array;
}

final void setArray(Object[] a) {
    array = a;
}

//Create an empty List
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

//The List created includes elements of the specified collection
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    //If the specified collection is also a CopyOnWriteArrayList, point the reference to the internal array of the collection
    if (c.getClass() == CopyOnWriteArrayList.class)
      elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
      elements = c.toArray();
      if (elements.getClass() != Object[].class)
        elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    //Set reference
    setArray(elements);
}

//Initialize CopyOnWriteArrayList by specifying an array, which cannot be directly referenced because the array can be modified at will.
//You must recreate an array from the specified array copy element to the new array
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

CopyOnWriteArrayList locks write operations by re-entrant locks, and implements internal storage through Object arrays.

When initializing an array by specifying it, a new array must be created, instead of referring directly to the parameter array, because the parameter array can be modified externally at will.

2. Increasing Method

add method

CopyOnWriteArrayList has many ways to add elements. These methods are all "write" methods. You need to lock them before you modify them. First, look at the simplest add method.

public boolean add(E e) {
    //Write operations need to be locked
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      Object[] elements = getArray();
      int len = elements.length;
      //Write-time replication!
      Object[] newElements = Arrays.copyOf(elements, len + 1);
      newElements[len] = e;
      setArray(newElements);
      return true;
    } finally {
      //Release lock
      lock.unlock();
    }
}

Before modifying the element, an array is copied through the Arrays.copyOf method and modified on the new array. After modification, the array is set to refer to the new array.

CopyOnWriteArrayList also has another way to add elements to a specified location. The idea is basically the same and will not be introduced.

Then look at the addAll method

addAll method

This method adds the elements of the specified collection to the CopyOnWriteArrayList and looks at the source code.

public boolean addAll(Collection<? extends E> c) {
    Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
      ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
    if (cs.length == 0)
      return false;
    final ReentrantLock lock = this.lock;
    //Write operation to lock!
    lock.lock();
    try {
      Object[] elements = getArray();
      int len = elements.length;
      //If the size of this array is 0, point the array reference to cs
      if (len == 0 && cs.getClass() == Object[].class)
        setArray(cs);
      else {
        //Write-time replication!
        //First copy the elements array to a larger array, and then copy the elements in cs to the larger array.
        Object[] newElements = Arrays.copyOf(elements, len + cs.length);
        System.arraycopy(cs, 0, newElements, len, cs.length);
        setArray(newElements);
      }
      return true;
    } finally {
      //Release lock
      lock.unlock();
    }
}

The method is simple, the key point is to look at the annotations.

There is also a method of inserting a specified set element in a specified location. The idea is basically the same and will not be explained.

addAllAbsent

This method has a set parameter c, which adds elements that do not exist in the CopyOnWriteArrayList to the CopyOnWriteArrayList. Look at the source code:

public int addAllAbsent(Collection<? extends E> c) {
    Object[] cs = c.toArray();
    if (cs.length == 0)
      return 0;
    final ReentrantLock lock = this.lock;
    //Write operations must be locked
    lock.lock();
    try {
      Object[] elements = getArray();
      int len = elements.length;
      int added = 0;
      //This for loop places elements that do not exist in the CopyOnWriteArrayList in c in the front of cs
      for (int i = 0; i < cs.length; ++i) {
        Object e = cs[i];
        //Neither does it exist in the current CopyOnWriteArrayList array nor in the cs array
        if (indexOf(e, elements, 0, len) < 0 &&
            indexOf(e, cs, 0, added) < 0)
          cs[added++] = e;
      }
      //Add elements to the CopyOnWriteArrayList array at once
      if (added > 0) {
        Object[] newElements = Arrays.copyOf(elements, len + added);
        System.arraycopy(cs, 0, newElements, len, added);
        setArray(newElements);
      }
      return added;
    } finally {
      //Release lock
      lock.unlock();
    }
}

AddiAllAbsent puts the elements that eventually need to be added to the CopyOnWriteArrayList into an array, and then adds them all at once.

3. Delete method

remove

//This method deletes the element at the specified index and returns the deleted element.
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      Object[] elements = getArray();
      int len = elements.length;
      E oldValue = get(elements, index);
      int numMoved = len - index - 1;
      //numMoved=0, indicating that the last element of the array was deleted
      if (numMoved == 0)
        setArray(Arrays.copyOf(elements, len - 1));
      else {
        Object[] newElements = new Object[len - 1];
        System.arraycopy(elements, 0, newElements, 0, index);
        System.arraycopy(elements, index + 1, newElements, index,
                         numMoved);
        setArray(newElements);
      }
      //Returns deleted elements
      return oldValue;
    } finally {
      lock.unlock();
    }
}

The deletion method is simple, and then look at another overloading method:

public boolean remove(Object o) {
    Object[] snapshot = getArray();
    //Find the location of element o
    int index = indexOf(o, snapshot, 0, snapshot.length);
    return (index < 0) ? false : remove(o, snapshot, index);
}

This method deletes the specified element instead of deleting it according to the index. First, it calls the indexOf method to find the index of the element in the array, and then it calls another overloaded deletion method to delete the element.

removeAll

This method has a parameter of a specified set and deletes all elements in this List that exist in the specified set. Look at the source code:

//Delete all elements that exist in set c and return whether the List has changed
public boolean removeAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      Object[] elements = getArray();
      int len = elements.length;
      if (len != 0) {
        int newlen = 0;
        Object[] temp = new Object[len];
        for (int i = 0; i < len; ++i) {
          Object element = elements[i];
          //Explains that the current element is not in the specified collection c, does not need to be deleted, and is placed in a temporary array
          if (!c.contains(element))
            temp[newlen++] = element;
        }
        //If the array changes, copy a temporary array and set the reference to that array
        if (newlen != len) {
          setArray(Arrays.copyOf(temp, newlen));
          return true;
        }
      }
      //Unchanged
      return false;
    } finally {
      lock.unlock();
    }
}

The idea of implementing this method is also simple. The elements existing in the specified set are placed in a temporary array. Finally, the temporary array is copied and the array reference of CopyOnWriteArrayList is reset.

CopyOnWriteArrayList also has a way to delete elements within a specified range, which is not explained.

4. iterator

The static class COWIterator inside CopyOnWriteArrayList implements an iterator. It is important to note that any modification operation of the iterator here throws an Unsupported OperationException, which cannot be modified through the iterator. Look at the source code:

static final class COWIterator<E> implements ListIterator<E> {
    //A snapshot of the original array
    private final Object[] snapshot;
    //Index of the next element
    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;
    }

    //The following modifications throw exceptions
    public void remove() {
      throw new UnsupportedOperationException();
    }

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

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

    //The idea of functional programming, traversing arrays and dealing with this element
    @Override
    public void forEachRemaining(Consumer<? super E> action) {
      Objects.requireNonNull(action);
      Object[] elements = snapshot;
      final int size = elements.length;
      for (int i = cursor; i < size; i++) {
        @SuppressWarnings("unchecked") E e = (E) elements[i];
        //Processing this element
        action.accept(e);
      }
      cursor = size;
    }
}

This iterator can only be viewed without task modification. Note the forEachRemaining method, which traverses the array elements and processes them. With regard to functional programming, you will have the opportunity to learn more in the future.

Posted by rainerpl on Tue, 21 May 2019 11:45:09 -0700