CopyOnWriteArrayList source code analysis and design thinking

Keywords: JDK

Introduce

In the comments of ArrayList, JDK tells us that when ArrayList (JDK 1.2) is used as a thread shared variable, it is unsafe for threads. We are recommended to lock or use connection. Synchronized List (), while Vector (JDK 1.0) is not recommended, because the premise of Vector to ensure thread safety is to add synchronized heavy lock, which seriously affects performance, So it provides a new thread safe List - copyonwritearraylist (JDK 1.5)

Features

1. Thread safety without lock
2. Ensure thread safety by using read-write lock + array copy + volatile
3. During the write operation, the array will be copied out and operated on the new array. After the operation is successful, the array will be assigned back
4. There is no need to copy during read operation. The idea of separation of read and write is used. However, the underlying array is modified with volatile, which is visible to threads
5. Consistent with the ArrayList data structure, the bottom layer is array

General induction

CopyOnWriteArrayList operates on arrays in four steps:
1. Lock reading and writing lock
2. Copy a new array from the original array
3. Operate on the new array and assign the new array to the original array container
4. Release the lock (finally release to prevent memory leakage)

Source code analysis

Underlying array container
private transient volatile Object[] array; //volatile decoration visible to threads
Construction method
	//The nonparametric construction will directly initialize an array and assign the array to the underlying array container
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

	/*
		A parameterized construct needs to pass a collection of subtypes that can receive E types or E

		If the type is the same as CopyOnWriteArrayList, its
		Array assigned to the new CopyOnWriteArrayList collection

		If they are not the same, convert them to an array and then copy the array
	*/
	public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
Tail added
 public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); //Lock up
        try {
            Object[] elements = getArray(); //Get the original array
            int len = elements.length; 
            //Copy new array new length + 1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //Add a new node at the end of the array
            newElements[len] = e;
            //New array overwrites the original array
            setArray(newElements);
            return true;
        } finally {
        	//Release lock
            lock.unlock();
        }
    }

Lock is used here to ensure that only one thread can add to the same array at the same time. In addition to lock, a new array will be created from the old array and the value will be copied instead of operating on the original array. There are new problems. Why copy the array instead of operating on the array?
As a result of:
volatile keyword modifies the original array. If you only modify the values of several nodes in the array, visibility will not be triggered. Therefore, you must modify the memory address of the array, that is to say, you need to reassign the array
The new array is copied, which has no effect on the old array. Only after the new array is copied, can it be accessed externally, which reduces the impact of the changes of the old array during the assignment process

New at index
public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();//Lock up
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //Judge the relationship between index and original array length
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            //New array
            Object[] newElements;
            int numMoved = len - index;
            //Index = add node directly at the end of array
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                //Array copy copy 0--index
                System.arraycopy(elements, 0, newElements, 0, index);
                //Array copy copy index+1 -- end
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //index is assigned to an empty node
            newElements[index] = element;
            //Array assignment
            setArray(newElements);
        } finally {
        	//Release lock
            lock.unlock();
        }
    }
Summary

1. Lock: ensure that the array can only be operated by one thread at the same time
2. Array copy: ensure the visibility of volatile triggered by array memory address modification
3.volatile: after the value is modified, other threads immediately perceive

Delete node at index
 public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock(); //Lock up
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index); //Old value
            //If you copy 0--len-1 directly at the end of the array
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
            //If you do not copy the array twice, the first time 0--len-1, the second time index+1 -- tail
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock(); //Release lock
        }
    }
Summary

1. lock up
2. Segmented copy
3. release lock

Batch deletion
public boolean removeAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock(); //Lock up
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (len != 0) {
                int newlen = 0;
                Object[] temp = new Object[len];//Create a new array
                for (int i = 0; i < len; ++i) {
                    Object element = elements[i];
                	//Place elements that do not contain the c set in the new array from 0
                    if (!c.contains(element))
                        temp[newlen++] = element;
                }
                if (newlen != len) {
                    setArray(Arrays.copyOf(temp, newlen));
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock(); //Release lock
        }
    }

From the source code, we can see that CopyOnWriteArrayList does not delete elements one by one, but circularly judges the array, putting the data we don't need to delete into the temporary array, and finally wetting the array is the data we don't need to delete
If you use delete one by one, you will copy the array once every time you delete it, which consumes time and performance. It takes a long time to lock and has a large concurrency, resulting in a large number of requests waiting for locks, which also takes up a certain amount of memory

Query IndexOf
  private static int indexOf(Object o Search object, Object[] elements Search target array,
                               int index Starting position, int fence End position) {
       //Query element is null traverse put back
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
        //Not null traversal
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }
iteration

During the iteration, if the original value of the array is changed, the ConcurrentModificationException will not be thrown. The reason is that the read and write are separated. Each operation will generate a new array without affecting the old array

summary

When we use List when the thread is unsafe, we recommend copyonwritearraylist. Copyonwritearraylist implements the thread safety of List by cooperating with lock + array copy + volatile

Published 19 original articles, won praise 6, visited 1950
Private letter follow

Posted by john-iom on Fri, 28 Feb 2020 00:15:00 -0800