Thread unsafe in ArrayList CopyOnWriteArrayList

Posted by PunjabHaker on Sat, 18 Dec 2021 11:11:11 +0100

CopyOnWriteArrayList import
Thread insecurity occurs when simulating the traditional ArrayList

public class Demo1 {
    public static void main(String[] args) {
        //List<String> list = new CopyOnWriteArrayList<>();
        List<String> list = new ArrayList<>();

        //Start 50 threads to add data to ArrayList
        for (int i = 1; i <= 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

The running results are as follows: due to the existence of the fail fast mechanism, the error of modcount modification exception is thrown (modcount is a variable in the source code of ArrayList, which is used to indicate the number of modifications, because ArrayList is not a collection class designed for concurrency)

How to solve this problem?

Method 1: the Vector set can be used. The Vector set is an ArrayList of thread safe version. Its methods are decorated with a layer of synchronized. The jvm built-in lock is adopted to ensure its atomicity, visibility and order in the case of concurrency. But it also brings performance problems, because once synchronized expands to heavyweight locks, there is a change in user state and mentality, and multithreaded context switching will bring overhead. Another problem is that the expansion of the Vector set is not as good as the ArrayList strategy

List<String> list = new Vector<>();

Method 2: use collections synchronizedList

List<String> list = Collections.synchronizedList(new ArrayList<>());

Method 3: adopt the concurrent container provided by JUC, CopyOnWriteArrayList

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

Analysis of CopyOnWriteArrayList
Like ArrayList, its underlying data structure is also an array. In addition, transient does not allow it to be serialized, and volatile decoration is added to ensure its visibility and order under multithreading
Let's look at its constructor first

  public CopyOnWriteArrayList() {
       //An array of size 0 is created by default
        setArray(new Object[0]);
    }

    final void setArray(Object[] a) {
        array = a;
    }
	
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        //If the current collection is of type CopyOnWriteArrayList, assign it directly
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
         	//Otherwise, call toArra() to turn it into an array   
            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);
        }
        //Set array
        setArray(elements);
    }
	
    public CopyOnWriteArrayList(E[] toCopyIn) {
        //Copy the passed in array elements to the current array
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
	

Let's take a look at several operations to read data. It can be seen that they are not locked, which is strange. How to ensure thread safety?

    final Object[] getArray() {
        return array;
    }
    public int size() {
        return getArray().length;
    }
   public boolean isEmpty() {
        return size() == 0;
    }
    public int indexOf(E e, int index) {
        Object[] elements = getArray();
        return indexOf(e, elements, index, elements.length);
    }
    public int lastIndexOf(Object o) {
        Object[] elements = getArray();
        return lastIndexOf(o, elements, elements.length - 1);
    }

Let's take a look at the add function when it is modified

   public boolean add(E e) {
        //Lock with ReentrantLock
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //Call getArray() to get the original array
            Object[] elements = getArray();
            int len = elements.length;
            //Copy the old array to get an array with length + 1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //Add elements and replace the original array with setArray() function
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

It can be seen that the modification operation is based on the fail safe mechanism. Like our String, we do not directly operate on the original object, but copy a copy to modify it. In addition, the modification operation here is locked with a Lock lock, so thread safety is guaranteed.

Let's take a look at the remove operation to see if it does so

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

private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    //Lock
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) findIndex: {
            int prefix = Math.min(index, len);
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            if (index >= len)
                return false;
            if (current[index] == o)
                break findIndex;
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        //Copy an array
        Object[] newElements = new Object[len - 1];
        System.arraycopy(current, 0, newElements, 0, index);
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        //Replace original array
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

It can be seen that the idea is the same. We compare it with ArrayList. It can be seen that its efficiency is much lower than ArrayList. After all, in a multi-threaded scenario, it needs to copy a copy based on the original array every time, which consumes memory and time. However, ArrayList is only expanded when its capacity is full. Therefore, in a non multi-threaded scenario, we'd better use ArrayList.

This also solves my previous question. Why do you still learn ArrayList? The CopyOnWriteArrayList of JUC version can do things that ArrayList can't do. It's also very fragrant for us to directly use CopyOnWriteArrayList.

Summary
CopyOnWriteArrayList is suitable for multi-threaded scenarios. It adopts the idea of separation of read and write. The read operation is not locked, the write operation is locked, and the write operation efficiency is low
CopyOnWriteArrayList is based on the fail safe mechanism. Each modification will copy a copy of the original basis and replace it after modification
CopyOnWriteArrayList is locked with ReentrantLock.

Topics: Java