CopyOnWriteArrayList 实现原理

Posted 盛夏温暖流年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CopyOnWriteArrayList 实现原理相关的知识,希望对你有一定的参考价值。

CopyOnWriteArrayList 是什么

CopyOnWriteArrayList 是 Java 并发包 java.util.concurrent 中提供的并发容器,本质上是一个线程安全且读操作无锁的 ArrayList。它在确保线程安全的前提下,通过牺牲写操作的效率来保证读操作的高效。

所谓 CopyOnWrite 就是通过复制的方式来完成对数据的修改,在修改时复制一个新的数组,在上面进行修改,不会对旧的数组进行改变,也就没有读写数据不一致的问题了。

CopyOnWriteArrayList 优缺点

优点

  • 读操作性能高

因为无需任何同步措施,比较适用于读多写少的并发场景。

  • 迭代器遍历不会抛出异常

遍历 ArrayList 时,若中途有别的线程对其修改,则会抛出 ConcurrentModificationException 异常。而 CopyOnWriteArrayList 由于其"读写分离"的思想,它的遍历和修改操作分别作用在不同的 list 容器上,所以在使用迭代器遍历时,不会抛出 ConcurrentModificationException 异常。

缺点

  • 内存占用大 

每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;

  • 无法保证实时性

CopyOnWriteArrayList 的写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞,所以读取到的是老容器的数据,无法保证数据的实时性。

 

CopyOnWriteArrayList 实现原理

如图所示,CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。

而进行写操作时,需要首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器,相对来说性能较差。 

CopyOnWriteArrayList 的关键数据结构如下:

/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

可以看到,CopyOnWriteArrayList 底层实现和 ArrayList 一样,用数组来保存元素,但它多了把独占锁 lock,来保证线程安全。

CopyOnWriteArrayList 关键方法分析

add 方法

    public boolean add(E e) {
        //ReentrantLock加锁,保证线程安全
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //拷贝原容器,长度为原容器长度加一
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新副本上执行添加操作
            newElements[len] = e;
            //将原容器引用指向新副本
            setArray(newElements);
            return true;
        } finally {
            //解锁
            lock.unlock();
        }
    }

 添加方法的逻辑很简单:

  1. 使用 ReentrantLock 加锁,保证线程安全;
  2. 对原始容器进行拷贝得到副本;
  3. 在新的副本上执行添加操作,添加完成后,将原始容器的引用指向新的副本;
  4. 执行解锁操作;

remove 方法

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

代码逻辑如下: 

  1. 加锁,保证多线程下的数据安全
  2. 获取到原始数组并定位到需要删除的 index 索引下标
  3. 如果要删除的是列表末端数据,拷贝前 len-1 个数据到新副本上,再切换引用;
  4. 否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用;
  5. 执行解锁操作;

get 方法

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

可以看到,它的 get 方法分为2步,先获取数组,再获取 index 位置的元素,这2步都是没有加锁的?为什么不需要加锁呢?

上面提到,add() 是先拷贝原数组,然后在拷贝的数组上操作的,在 setArray() 之前对原数组并没有影响,因此读的时候不需要加锁。虽然不需要加锁,但会出现数据弱一致性问题,也就是说 CopyOnWriteArrayList 只能保证数据的最终一致性

 参考博客如下,非常感谢:

https://segmentfault.com/a/1190000039065671

CopyOnWriteArrayList实现原理及源码分析 - dreamcatcher-cx - 博客园

以上是关于CopyOnWriteArrayList 实现原理的主要内容,如果未能解决你的问题,请参考以下文章

CopyOnWriteArrayList实现原理及源码分析

CopyOnWriteArrayList 实现原理

CopyOnWriteArrayList实现原理源码分析

CopyOnWriteArrayList实现原理源码分析

CopyOnWriteArrayList实现原理源码分析

CopyOnWriteArrayList实现原理以及源码解析