1. 为何须要 CopyOnWriteArrayList数组
ArrayList 的内部实现是一个数组, 而且是动态扩容的, 当插入数据时, 先判断数组是否须要扩容, 若是须要扩容, 则先扩容, 再插入数据, 也就说插入由三步组成并发
1) 检查是否须要扩容spa
2) 扩容/不扩容线程
3) 数据加入到数组code
代码以下blog
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
这里若是出现并发操做, 会有两个问题ci
1) 若是同时进行扩容, 则有可能出现连续进行两次扩容的问题, 而实际只须要一次element
2) 若是同时对数组进行赋值, 则有可能第一个赋值元素被覆盖, 由于可能两个线程拿到的 size 是同样的, 他们都填到数组的同一个槽里rem
再看另外一个 add 操做get
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
这种状况下, add 分为四步
1) 检查是否须要扩容
2) 扩容
3) 移动数据
4) 插入数据
若是此时有并发的读取和插入操做, 则有可能出现读取到的值为 null 的状况, 例如 list.get(3) 跟 list.add(3, "new") 同时发生, 原本 list.get(3) 应该拿到 "old" 或者 "new", 如今却拿到了 null, 这是由于在取值的过程当中正好发生了移动数据, 可是数据又还没被插入到移动的空槽里
2. 如何解决这些问题?
一种最简单的方式是对 ArrayList 的全部行为所有加锁, 例如 Collections.synchronizedList(list) 方法, 他会包装 list, 并对全部操做加锁
可是这种方式会 block 全部操做, 读, 写 都是串行的, 会影响效率
3. CopyOnWriteArrayList 如何解决这些问题
cowlist 的写操做全都加锁, 而且在加锁后会将底层数组复制一份再进行写操做, 当写操做完成之后, 整个替换底层数组
1) 使用锁, 即解决了并发写的问题
2) 读操做不加锁, 效率更高, 读写不冲突
3) 写操做使用副本控制, 解决读操做会读到 null 问题, 由于底层数据不会出现有空槽的中间状态