The Beauty of Java Concurrent Programming Chapter 5 Reading Notes

Analysis of concurrent List source code in java concurrent package

introduce

CopyOnWriteArrayList

For thread-safe ArrayList, the modification operations are performed on a copied array (snapshot) at the bottom layer, that is, the copy-on-write strategy

Class Diagram

Each object has an array array to store specific elements. The ReentrantLock exclusive lock object is used to ensure that only one thread modifies the array at the same time. Here, just remember that ReentrantLock is an exclusive lock, and only one thread can acquire it at the same time.

Source code analysis of the main method

initialization

The no-argument constructor creates a zero-sized object array

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

Parameterized construction

//创建一个list,内部是入参toCopyIn副本
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
//入参为集合,将集合的元素复制到笨list
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        if (c.getClass() != ArrayList.class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

add element

public boolean add(E e) {
  	//获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      	//获取array
        Object[] elements = getArray();
				//复制array到新数组,添加元素到新数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
      	//使用新数组替换添加前的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

The thread that calls the add method will first execute code (1) to acquire an exclusive lock. If multiple programs call the add method, only one thread will acquire the lock, and other threads will be blocked and suspended until the lock is released. So one thread After acquiring the lock, it is guaranteed that other threads will not modify amay during the process of adding elements to this thread.

After the thread acquires the lock, execute the code (2) to obtain the array, then execute the code (3) to copy the array to a new array (M here can know that the size of the new array is the size of the original array increased by 1, so CopyOnWriteArayList is unbounded), and put the new The incremented elements are added to the new array. Then execute code (4) to replace the original array with the new array, and release the lock before returning. Because of the lock, the stitching process is an atomic operation. It should be noted that when adding elements, a snapshot is first copied, and then added to the snapshot instead of directly on the original array.

Get the specified position element

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

When thread x calls the get method to obtain the element at the specified position, it takes two steps, first obtains the array array (step A), and then accesses the element at the specified position through the subscript (step B). These two steps do not perform lock synchronization

Without locking, another thread y performs a remove operation after executing step A and before executing step B. Assuming that the element 1 to be deleted is currently removed, the remove operation will first acquire an exclusive lock, and then perform a copy-on-write operation. That is to copy the current array array, and then delete the element 1 that thread x wants to access through the get method in the auxiliary array, and then let array point to the copied array, and at this time, the reference count of the array pointed to by array before is 1 instead of 0 , because thread x is still using it, at this time the thread starts to execute step B, and step B operates the array before thread y deletes the element

Modify the specified element

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

First obtain an exclusive lock to prevent other threads from modifying the array array, then obtain the current array, and call the get method to obtain the element at the specified position. If the element value at the specified position is inconsistent with the new value, create a new array and copy the elements, and then in Modify the element value at the specified position on the new array and set the new array to array. If the element value at the specified position is the same as the new value, in order to ensure the volatile semantics, the array must be reset

delete 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;
        //如果要删除的是最后一个元素
        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);
        }
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}

Weakly consistent iterators

public class IteratorTest {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
        arrayList.add("hello");
        arrayList.add("abbbb");
        Iterator<String> iterator=arrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

After the iterator is returned, additions, deletions, and modifications to the list by other threads are invisible to the iterator

public Spliterator<E> spliterator() {
            return Spliterators.spliterator
                    (getArray(), Spliterator.IMMUTABLE | Spliterator.ORDERED);
        }
        
        static final class COWIterator<E> implements ListIterator<E> {
            //array的快照版本
            private final Object[] snapshot;
            //下标
            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++];
            }

When the iterator0 method is called to get the selector, a COWIterator object will actually be returned. The snapshot variable of the COWIterator object saves the content of the current list, and the cursor is the data subscript when traversing the list.

Why is snapshot a snapshot of list?

Obviously it is a reference passed by a pointer, not a copy. In the process of traversing elements using the returned iterator, other threads do not add, delete, or modify the list, so the snapshot itself is the array of the list because they are reference relationships. However, if other threads add, delete, or modify the list during the traversal, then the snapshot is a snapshot, because the array in the list is replaced by the new array after the addition, deletion, and modification. At this time, the old array is referenced by the snapshot. This also shows that after obtaining the iterator, when using the iterator element, other threads will not be able to add, delete, or modify the list, because they operate on two different arrays

/**
 * @author xingchen
 * @version V1.0
 * @Package com.并发.Test
 * @date 2023/4/21 13:42
 * 
 * 学习弱一致性案例
 */
public class CopyListTest {
    
    private static volatile CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException{
        arrayList.add("hello");
        arrayList.add("hello");
        arrayList.add("hello");
        arrayList.add("hello");
        arrayList.add("hello");
        arrayList.add("hello");
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.set(1,"baba");
                arrayList.remove(2);
                arrayList.remove(3);
            }
        });


        Iterator<String > iterator=arrayList.iterator();
        thread.start();
        thread.join();
        
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        
    }
}

Summarize

CopyOnWriteArrayList uses the copy-on-write strategy to ensure the consistency of the list, and the acquisition-modification-write three-step operation is not atomic, so exclusive locks are used when adding, deleting and modifying to ensure that there is only one thread at a certain time The list can be modified. In addition, the weakly consistent iterator provided by CopyOnWriteArrayList ensures that after the iterator is obtained, the modification of the list by other threads is invisible. The array traversed by the iterator is a snapshot, and the bottom layer of CopyOnWriteArrayList uses it realized

Guess you like

Origin blog.csdn.net/weixin_60257072/article/details/130513870