CopyOnWriteArrayList的实现原理
CopyOnWriteArrayList
第一节 概述
1.1 为什么需要CopyOnWriteArrayList?
多读场景需要高效的并发数组。
常用的 ArrayList
不是一个线程安全的集合,基于快速失败机制,多线程操作会抛出 ConcurrentModificationException
,而线程安全的 Vector
又因为低效的设计而被弃用,又或者 Collections.synchronizedList()
来获取一个包装的线程安全类,与前者一样使用 synchronized
来保证的线程安全。
synchronized
这种独占锁在同一时刻只能有一个线程拥有对象监视器,对于大部分读多写少的业务场景并不适用。针对这种场景而设计的读写锁 ReentrantReadWriteLock
是一个合适的选择。
如果只是在 List
上封装一层读写锁,读数据仍会被其他操作阻塞,所以需要并发包提供的 CopyOnWriteArrayList
。
1.2 什么是CopyOnWrite?
CopyOnWrite
(COW)即写时复制,通过延迟更新的策略来实现数据一致性,并且能保证线程间不阻塞。
通俗的讲就是在向容器添加元素时,并非直接向容器内添加,而是先将当前容器进行Copy,复制出一个新容器,然后向新容器添加元素,完成后再将指向旧容器的引用指向新容器。
这一过程的优势就是在进行并发的读操作时不用加锁,因为旧容器并不改变,所以写时复制也是一种读写分离的思想,通过放弃数据的实时性来达到数据一致性,获得的是提高并发性。
1.3 写时复制和读写锁的区别?
相同点:两者都是通过读写分离思想实现,都实现了读线程间互不阻塞。
不同点:使用读写锁依然会存在线程阻塞等待的情况,而COW则牺牲了数据实时性,保证读线程不会存在等待。
读写锁中,读线程为了实现数据实时性,在写锁被获取时,读线程会等待。而写时复制中虽然写线程最终的修改结果可以被读线程感知,但有一定延迟。
第二节 实现原理
2.1 基本结构
根据其接口实现 RandomAccess
就可得知,其底层为数组。
1 | public class CopyOnWriteArrayList<E> |
通过可重入锁 ReentrantLock
来保证线程安全,通过 volatile
修饰底层数组来保证可见性。
2.2 get
get()
方法源码如下,读操作不需要考虑并发安全,直接通过数组索引获取元素。
1 | /** |
2.3 add
add()
方法源码如下,写线程需要阻塞。
1 | /** |
实现核心主要是:
- 通过
ReentrantLock
保证同一时刻只有一个写线程在进行数组的复制,否则会导致内存中存在多份复制数组。 volatile
修饰的底层数组,根据happens-before
规则,写线程对数组引用的修改对读线程是可见的。
add()
还有重载方法,指定位置添加元素:
1 | /** |
核心就是通过 System.arraycopy()
进行错位复制。
2.4 set
set()
方法源码如下,写线程需要阻塞。
1 | /** |
2.5 remove
remove()
方法源码如下,写线程需要阻塞。
1 | /** |
和 set()
、add()
等方法一样,remove()
仍通过数组浅复制进行删除操作,主要是错位复制。
参考:
🔗 《Java并发编程的艺术》