CopyOnWriteArrayList
1. 概述
1.1 简介
CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过数组实现,每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。
1.2 继承体系
主要是继承了List接口,提供一系列根据索引操作数组的方法
2. 源码解析
2.1 属性字段
// 写时的加锁, 读不加锁 final transient Object lock = new Object(); // 存储元素的数组 private transient volatile Object[] array; // 所有对array的操作都是通过getArray()和setArray() final Object[] getArray() { return array; } final void setArray(Object[] a) { array = a; }
2.2 构造函数
// 默认构造函数 public CopyOnWriteArrayList() { setArray(new Object[0]); } // 集合构造 public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] es; if (c.getClass() == CopyOnWriteArrayList.class) // 因为是读写分离的, 不必担心此集合被其它集合修改问题 // 因此直接指向其数组 es = ((CopyOnWriteArrayList<?>)c).getArray(); else { es = c.toArray(); // 其它类型集合不能保证, 因此需要复制 if (es.getClass() != Object[].class) es = Arrays.copyOf(es, es.length, Object[].class); } setArray(es); } // 直接传递数组也需要复制 public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
2.3 常规操作
// 尾插 public boolean add(E e) { // 写时加锁, 保证写的线程安全 synchronized (lock) { Object[] es = getArray(); int len = es.length; // 数组长度和元素数量是一致的 // 添加就是重新创建一个数组 es = Arrays.copyOf(es, len + 1); es[len] = e; setArray(es); return true; } }
// 插入指定位置 public void add(int index, E element) { // 写时加锁 synchronized (lock) { Object[] es = getArray(); int len = es.length; if (index > len || index < 0) throw new IndexOutOfBoundsException(outOfBounds(index, len)); Object[] newElements; int numMoved = len - index; // 尾插 if (numMoved == 0) newElements = Arrays.copyOf(es, len + 1); // 非尾插 else { // 新建容量+1的数组, 复制原数组元素 newElements = new Object[len + 1]; System.arraycopy(es, 0, newElements, 0, index); System.arraycopy(es, index, newElements, index + 1, numMoved); } // 添加元素 newElements[index] = element; // setArray(newElements); } }
// 移除元素 public E remove(int index) { // 加锁 synchronized (lock) { Object[] es = getArray(); int len = es.length; E oldValue = elementAt(es, index); int numMoved = len - index - 1; Object[] newElements; // 尾部移除, 直接缩容 if (numMoved == 0) newElements = Arrays.copyOf(es, len - 1); else { // 新建容量-1数组, 并复制 newElements = new Object[len - 1]; System.arraycopy(es, 0, newElements, 0, index); System.arraycopy(es, index + 1, newElements, index, numMoved); } setArray(newElements); return oldValue; } }
// 这个是线程安全的? public boolean remove(Object o) { Object[] snapshot = getArray(); int index = indexOfRange(o, snapshot, 0, snapshot.length); // 调用remove之前数组实际上已经被改变了呢? return index >= 0 && remove(o, snapshot, index); }
// get不用加锁, 即读不加锁 public E get(int index) { return elementAt(getArray(), index); } // 只要有改变元素内容的都要加锁 public E set(int index, E element) { // 加锁 synchronized (lock) { Object[] es = getArray(); E oldValue = elementAt(es, index); if (oldValue != element) { es = es.clone(); es[index] = element; } // 没有数组长度的增加与减少, 设置的数组还是原数组 setArray(es); return oldValue; } }
1 public boolean remove(Object o) { 2 Object[] snapshot = getArray(); 3 int index = indexOfRange(o, snapshot, 0, snapshot.length); 4 // 有元素才会进行处理, 不过因为上面没有加锁, 在此时集合的array可能已经改变了 5 return index >= 0 && remove(o, snapshot, index); 6 } 7 8 // ??, 反正是删除 9 private boolean remove(Object o, Object[] snapshot, int index) { 10 // 加锁 11 synchronized (lock) { 12 // 重新获取array 13 Object[] current = getArray(); 14 int len = current.length; 15 // array已经被改变, 即快照无效 16 if (snapshot != current) findIndex: { 17 // min(index, len)之间查询, 因为可能缩容了 18 int prefix = Math.min(index, len); 19 for (int i = 0; i < prefix; i++) { 20 // 21 if (current[i] != snapshot[i] 22 && Objects.equals(o, current[i])) { 23 // 提前定位到数据位置了, 而且这个数据和快照的数据还不是同一个对象 24 index = i; 25 break findIndex; 26 } 27 } 28 // 没有数据 29 if (index >= len) 30 return false; 31 // 32 if (current[index] == o) 33 break findIndex; 34 // 35 index = indexOfRange(o, current, index, len); 36 if (index < 0) 37 return false; 38 } 39 // 常规删除 40 Object[] newElements = new Object[len - 1]; 41 System.arraycopy(current, 0, newElements, 0, index); 42 System.arraycopy(current, index + 1, 43 newElements, index, 44 len - index - 1); 45 setArray(newElements); 46 return true; 47 } 48 }
1 public int size() { 2 return getArray().length; 3 }
3. 总结
- 使用synchronized加锁,保证线程安全
- 写操作涉及到容量改变的都会加锁,并新建数组并拷贝数据,并在新数组中增删
- 写操作不涉及到容量如set只是加锁
- 使用读写分离的思想,读不加锁,写加锁
- 写操作基本都是要加锁、要新建数组、要拷贝数据的,因此效率很低,适合于读多写极少的情况
- 只保证最终一致性,不保证实时一致性(读时读到的可能是旧数组,而新数组还在处理中)
- 没有size属性,因为数组容量和数组长度是相同的