[Learning Notes - Java Collection - 3] CopyOnWriteArrayList Source Code Analysis

Posted by shahryar on Sat, 10 Aug 2019 13:21:53 +0200

brief introduction

CopyOnWriteArrayList is a thread-safe version of ArrayList. It is also implemented internally through arrays. Every modification to the arrays completely copies a new array to modify, and then replaces the old arrays. This ensures that only blocking write operations, not blocking read operations, and achieves read-write separation.

Inheritance System

  1. CopyOnWriteArrayList implements List, Random Access, Cloneable, java.io.Serializable and other interfaces.
  2. CopyOnWriteArrayList implements List, providing basic operations such as add, delete, traverse, etc.
  3. CopyOnWriteArrayList implements Random Access and provides random access capabilities.
  4. CopyOnWriteArrayList implements Cloneable and can be cloned.
  5. CopyOnWriteArrayList implements Serializable and can be serialized.

Source code parsing

attribute

/** Locking for modification */
final transient ReentrantLock lock = new ReentrantLock();

/** Where elements are actually stored, they can only be accessed through getArray()/setArray(). */
private transient volatile Object[] array;

  1. Lock is used to lock when modifying, and transient s are used to denote non-automatic serialization.
  2. Where array actually stores elements, it uses transient s to denote non-automatic serialization, and volatile s to denote that one thread modifies this field and another thread immediately visible.

Construction of CopyOnWriteArrayList()

Create empty arrays

public CopyOnWriteArrayList() {
    // All operations on array are done through setArray() and getArray().
    setArray(new Object[0]);
}

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

Construction Method of CopyOnWriteArrayList

If c is a CopyOnWriteArrayList type, assign its array directly to the current list array. Note that this is a shallow copy. Both sets share the same array.
If c is not a CopyOnWriteArrayList type, copy all elements of c to the current list array.


public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        // If c is also CopyOnWriteArrayList type
        // So just take its array and use it.
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // Otherwise, call its toArray() method to convert collection elements into arrays
        elements = c.toArray();
        // Here c.toArray() does not necessarily return an Object [] type
        // See the analysis in Array List for detailed reasons.
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

Construction Method of CopyOnWriteArrayList(E[] toCopyIn)

Copy the toCopyIn element to the current list array.

public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

Add (E) method

Add an element to the end.

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // Lock up
    lock.lock();
    try {
        // Getting old arrays
        Object[] elements = getArray();
        int len = elements.length;
        // Copy old array elements into new arrays
        // The new array size is the old array size plus 1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // Put the element last
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        // Release lock
        lock.unlock();
    }
}
  1. Locking;
  2. Get an array of elements;
  3. Create a new array, the size of which is the length of the original array plus 1, and copy the elements of the original array to the new array.
  4. Put the newly added element at the end of the new array.
  5. The new array is assigned to the array attribute of the current object, which overrides the original array.
  6. Unlock;

add(int index, E element) method

Add an element at the specified index.


public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // Lock up
    lock.lock();
    try {
        // Getting old arrays
        Object[] elements = getArray();
        int len = elements.length;
        // Check if it crosses the boundary, it can be equal to len
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            // If the insertion position is the last one
            // Copy an array of n+1, and the first n elements are the same as the old array.
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // If the insertion position is not the last one
            // So create an array of n+1
            newElements = new Object[len + 1];
            // Copy the elements of index before the old array into the new array
            System.arraycopy(elements, 0, newElements, 0, index);
            // Move index and its subsequent elements back one copy to the new array
            // That's exactly where the index is empty.
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // Place the element at index
        newElements[index] = element;
        setArray(newElements);
    } finally {
        // Release lock
        lock.unlock();
    }
}
  1. Locking;
  2. Check whether the index is legitimate, and if the IndexOutOfBoundsException exception is thrown illegally, note that index equals len is also legitimate here.
  3. If the index equals the length of the array (that is, the last bit of the array plus 1), copy an array of len+1.
  4. If the index is not equal to the length of the array, then create an array of len+1 and divide it into two parts according to the index position. The part before the index (not included) is copied to the part before the index of the new array (not included), and the position after the index is copied to the position after the index of the new array (not included), where the index is located. Leave empty in position.
  5. The index position is assigned to the element to be added.
  6. The new array is assigned to the array attribute of the current object, which overrides the original array.
  7. Unlock;

AddiIfAbsent (E) Method

Add an element if it does not exist in the collection.

public boolean addIfAbsent(E e) {
    // Get an array of elements, named snapshot
    Object[] snapshot = getArray();
    // Check that if the element does not exist, return false directly
    // If there is an additional call to the addIfAbsent() method to add elements
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    // Lock up
    lock.lock();
    try {
        // Retrieving old arrays
        Object[] current = getArray();
        int len = current.length;
        // If the snapshot does not match the newly acquired array
        // Note amended
        if (snapshot != current) {
            // Re-check whether the element is in the newly acquired array
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                // It's in this method to show that the element is not in the snapshot.
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        // Copy an array of n+1
        Object[] newElements = Arrays.copyOf(current, len + 1);
        // Put the element last
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        // Release lock
        lock.unlock();
    }
}
  1. Check if this element exists in the array snapshot;
  2. If there is a direct return false, if there is no call to addIfAbsent (E, Object [] snapshot) processing;
  3. Locking;
  4. If the current array is not equal to the incoming snapshot, indicate that there is a modification, check whether the element to be added exists in the current array, and return false directly if it exists.
  5. Copy a new array, the length is equal to the length of the original array plus 1, and copy the elements of the original array into the new array;
  6. Add the new element to the last bit of the array;
  7. The new array is assigned to the array attribute of the current object, which overrides the original array.
  8. Unlock;

get(int index)

Gets the elements of the specified index to support random access with time complexity of O(1).


public E get(int index) {
    // Getting elements does not require locks
    // Elements that return directly to the index position
    // There is no cross-check here, because the array itself does cross-check.
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

private E get(Object[] a, int index) {
    return (E) a[index];
}

remove(int index) method

Delete the element at the specified index location.

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    // Lock up
    lock.lock();
    try {
        // Getting old arrays
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            // If the last one is removed
            // Then copy a new array of n-1 directly and delete the last bit automatically.
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // If the last one is not removed
            // So create a new n-1 array
            Object[] newElements = new Object[len - 1];
            // Copy the elements of the former index into the new array
            System.arraycopy(elements, 0, newElements, 0, index);
            // Move the element after index (not included) one bit forward
            // This just overrides the index position, which is equivalent to deleting it.
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        // Release lock
        lock.unlock();
    }
}
  1. Locking;
  2. Gets the old value of the specified index location element;
  3. If the last element is removed, the first len-1 element of the original array is copied into the new array, and the new array is assigned to the array attribute of the current object.
  4. If the last element is not removed, a len-1 length array is created, and all the elements of the original array except the specified index position are copied into the new array, and the new array is assigned to the array attribute of the current object.
  5. Unlock and return the old value;

size() method

Returns the length of the array.


public int size() {
    // Getting the number of elements does not require locks
    // Returns the length of the array directly
    return getArray().length;
}

summary

  1. CopyOnWriteArrayList uses ReentrantLock to re-lock and ensure thread safety.
  2. CopyOnWriteArrayList has to copy a new array first, make modifications in the new array, replace the old array with the new array after modification, so the space complexity is O(n), and the performance is relatively low.
  3. The read operation of CopyOnWriteArrayList supports random access with O(1) time complexity.
  4. CopyOnWriteArrayList adopts the idea of separation of reading and writing. Reading operations are unlocked, writing operations are locked, and writing operations occupy large memory space, so it is suitable for occasions where reading is more and writing is less.
  5. CopyOnWriteArrayList only guarantees the final consistency, but does not guarantee the real-time consistency.

Topics: Java snapshot Attribute less