《Java并发编程之美》读书笔记二【CopyOnWriteArrayList】

一、并发List简答介绍

  • 因为并发包中只有CopyOnWriteArrayList ,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;

CopyOnWriteArrayList 对象里面有一个array数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对array进行修改。

  • 写时复制的线程安全的list需要注意以下几点:

何时初始化list,初始化的list 元素个数为多少,list是有限大小吗?
如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的?
如何保证使用迭代器遍历List时的数据一致性?

二、源码解析

1、初始化:
  • 无参构造函数
public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

代码内部创建了一个大小为 0 的 Object数组作为Array的初始值。

  • 有参构造函数:
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

创建一个 List 其内部元素是入参toCopyIn的副本

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);
    }

入参是一个集合,将集合里的元素复制到本List

2、添加元素:

在这里插入图片描述

  • 上图中的原理类似主要看一下add(E e)
public boolean add(E e) {
		//获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//获取array
            Object[] elements = getArray();
            int len = elements.length;
            //复制array 到新数组,
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加元素到新数组
            newElements[len] = e;
            //使用新数组替换添加前的数组
            setArray(newElements);
            return true;
        } finally {
        	//释放独占锁
            lock.unlock();
        }
    }
//获取数组array
final Object[] getArray() {
        return array;
    }
//替换数组
final void setArray(Object[] a) {
        array = a;
    }

调用add 方法的线程会首先执行代码中获取独占锁的部分,进行锁的获取,如果duoge线程都调用add 方法则只有一个线程会获取到该锁,其他线程会被阻塞挂起直到锁被释放。
所以一个线程获取到锁后,就保证了在该线程添加元素的过程中其他线程不会对array进行修改。
获取锁后,获取数组,获取到数组后,将该数组复制到一个新的数组【将原来的数组长度➕1】,将新增的元素添加到新数组。
使用新数组替换原数组,并在返回前释放锁,由于加了锁,整个add 过程是个原子性的操作,【注意: 添加元素的时候,是在 副本上进行的操作,而不是在原数组上进行操作】。

3、获取指定位置元素:
	/**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
  • getArray 和上面的一样的,看一下 get方法:
	@SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

使用get(int index) 获取下标为index 的元素,如果元素不存在则抛出IndexOutOfBoundsException异常。
在这里插入图片描述

当线程X 调用get 方法获取指定位置的元素时,分两步: 1、首先获取array数组【一】,2、通过下标访问指定位置的元素【2】。但是我们要注意到整个过程并没有加锁同步。

因为这两部都没有加锁,可能线程X执行完步骤一 之后,在步骤2 之前,另外一个线程y 进行了remove 操作,假设删除元素1 ,remove会首先获取独占锁,然后进行写时复制操作,也就是复制一份当前array数组,然后在复制的数组里删除 1 这个元素,之后让array 指向复制的数组,而此时,array 之前指向的数组的引用计数器为1 而不是0 ,因为线程X 还在使用它,这时线程X 开始执行步骤2,步骤2 操作的数组是 线程Y 删除元素之前的数组。

  • 所以虽然Y 已经删除了index 处的元素,但是线程X的步骤还是会返回index 处的锁,这是写时复制策略产生的弱一致性问题。
4、修改指定元素:
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();
        }
    }

首先获取独占锁,从而阻止其他线程对array数组进行修改,然后获取当前数组,并调用get方法获取指定位置的元素,如果指定位置的元素值与新值不一致则创建新数组并复制元素,然后在新数组上修改指定位置的元素值并设置新数组到array,如果指定位置的元素值与新值一样,则为了保证volatile的语义,还是需要重新设置array,虽然array 的内容并没有改变。

5、删除元素:

在这里插入图片描述

  • 删除list中指定元素可以使用上图中的方法,原理大致一样,主要讲一下remove(int index)
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();
        }
    }

类似于新增元素的方法,首先获取独占锁,保证删除数据期间其他线程无法对array进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的老数组,最后在返回前释放锁。

6、弱一致性的迭代器:

所谓的弱一致性是指返回迭代器后,其他线程对List的删除改对迭代器是不可见的。前面在获取指定位置的元素试提到过。
使用iterator获取迭代器时会返回COWIterator 对象,COWIterator 存储着一个array的快照,snapshot,说snapshot是list的快照的原因是:遍历元素的过程中,其他线程没有对list进行增删改,那么snapshot本身就是list的array,因为它们是引用关系,但是如果遍历期间其他线程对该list进行了增删改,那么snapshot就是快照了,因为增删改后,list里的数组被新数组替代了,这时候老数组被snapshot引用。

这就说明获取迭代器后,使用该迭代器元素时,其他线程对该list进行的增删改不可见。因为操作的其实是两个数组,这就是弱一致性。

  • 这个地方和之前的获取指定位置的元素很像。

三、总结

CopyOnWriteArrayList 使用写时复制的策略来保证list的一致性,而 获取 - 修改 - 写入 这三个步骤并不是原子的,所以在增删改的过程中都使用了独占锁,来保证某个某个时间内只有一个线程对list数组进行修改。CopyOnWriteArrayList 还提供了 弱一致性的迭代器从而保证在获取迭代器后,其他线程对list的修改是不可见的。迭代器遍历的数组是一个快照。

CopyOnWriteArraySet 底层使用 CopyOnWriteArrayList 实现的。

发布了122 篇原创文章 · 获赞 32 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/YangzaiLeHeHe/article/details/100159856