【源码粗读】 - CopyOnWriteList

150 阅读4分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

CopyOnWriteArrayList

属于JUC中的类

类头声明

 public class CopyOnWriteArrayList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

类变量

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

初始化代码块

 private void resetLock() {
     UNSAFE.putObjectVolatile(this, lockOffset, new ReentrantLock());
 }
 private static final sun.misc.Unsafe UNSAFE;
 private static final long lockOffset;
 static {
     try {
         UNSAFE = sun.misc.Unsafe.getUnsafe();
         Class<?> k = CopyOnWriteArrayList.class;
         lockOffset = UNSAFE.objectFieldOffset
             (k.getDeclaredField("lock"));
     } catch (Exception e) {
         throw new Error(e);
     }
 }

方法

构造方法

 public CopyOnWriteArrayList() {
     setArray(new Object[0]);
 }
 ​
 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);
 }
 ​
 final void setArray(Object[] a) {
     array = a;
 }
 ​
 public CopyOnWriteArrayList(E[] toCopyIn) {
     setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
 }

看到:

  elements = ((CopyOnWriteArrayList<?>)c).getArray();
  ......
  setArray(elements);
  }

显然会有个问题:

  • 如果是从相同的 CopyOnWriteArrayList创建而来,那么两个容器中的数组指向的是同一个数组:可以写一个demo来验证这一点。

如何保证从其他的CopyOnWriteArrayList创建而来的容器,是相对隔离可用的呢?【答案:快照】

基本方法

此处的基本方法,指的是:

  • get
  • set
  • add
  • remove

以及其他数组托管容器应该实现的基本操作。

get方法

 private E get(Object[] a, int index) {
         return (E) a[index];
     }
 ​
 public E get(int index) {
     return get(getArray(), index);
 }

get方法本身是原子性的,也不会对其他操作产生影响,因此不做任何lock和copy操作。

set方法

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

从这段代码上看,set方法做了如下工作

  • 对于该容器对象而言,set方法是加锁的。

  • set方法对新旧值做了比较

    • 如果是新旧值不相等(此处用的相等是**=而非equals**,对于对象而言指的是地址),那么会:

      • 复制一份新的数组
      • 新数组的目标索引处的值修改为目标值
      • 将托管的数组设置为新的数组
    • 如果不相等,那么将elements数组再塞回去。原因:

      • Not quite a no-op; ensures volatile write semantics,即:保证volatile写语义

      • 这篇博客中详细介绍了为什么:是为了保证happens-before原则。
      • 这个地方将变量再塞回去,保证了外部(调用该方法的地方)的上下文不会被CPU进行重排序。

add方法

 public boolean add(E e) {
     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();
     }
 }
 ​
 //指定索引位置插入
     public void add(int index, E element) {
         final ReentrantLock lock = this.lock;
         lock.lock();
         try {
             Object[] elements = getArray();
             //length和index的关系就不用多说了8
             int len = elements.length;
             if (index > len || index < 0)
                 throw new IndexOutOfBoundsException("Index: "+index+
                                                     ", Size: "+len);
             Object[] newElements;
             int numMoved = len - index;
             //如果正好是往数组结束位置插入
             if (numMoved == 0)
                 newElements = Arrays.copyOf(elements, len + 1);
             else {
                 //如果不是:前后复制,将index的地方空出来
                 newElements = new Object[len + 1];
                 System.arraycopy(elements, 0, newElements, 0, index);
                 System.arraycopy(elements, index, newElements, index + 1,
                                  numMoved);
             }
             newElements[index] = element;
             setArray(newElements);
         } finally {
             lock.unlock();
         }
     }

add方法中,同样做了2个工作,保证该操作在多线程下的准确性:

  • 使用ReentrantLock上锁
  • 复制扩容数组(长度+1),在将扩容后的最后一位设置为目标对象后,将托管数组设置为新的数组。

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();
     }
 }
 ​
 //目标删除
 public boolean remove(Object o) {
     Object[] snapshot = getArray();
     int index = indexOf(o, snapshot, 0, snapshot.length);
     return (index < 0) ? false : remove(o, snapshot, index);
 }
 ​
 //上面方法的一部分
 private boolean remove(Object o, Object[] snapshot, int index) {
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         Object[] current = getArray();
         int len = current.length;
         if (snapshot != current) findIndex: {
             int prefix = Math.min(index, len);
             for (int i = 0; i < prefix; i++) {
                 if (current[i] != snapshot[i] && eq(o, current[i])) {//--------1
                     index = i;
                     break findIndex;
                 }
             }
             if (index >= len)
                 return false;
             if (current[index] == o)//------------------2
                 break findIndex;
             index = indexOf(o, current, index, len);//---------------3
             if (index < 0)
                 return false;
         }
         Object[] newElements = new Object[len - 1];
         System.arraycopy(current, 0, newElements, 0, index);
         System.arraycopy(current, index + 1,
                          newElements, index,
                          len - index - 1);
         setArray(newElements);
         return true;
     } finally {
         lock.unlock();
     }
 }
 ​
 //indexOf就不介绍了,使用for循环和==(null值)/equals(非null值)进行比较并返回索引位置

remove方法也做了和上述两个方法相同的操作:

  • 上锁
  • 复制数组

在查找-remove的方法中,有一个小细节:

  • 如果查找到了,进了remove具体方法再上锁,且入参中包含了查找时的数组快照。

  • 如果快照和当前的托管数组不同,就进行比较:

    • 只有当前数组包含要remove的元素时才会进行下一步操作。(1,2,3)

使用了label代码块的技巧,快速跳出判断。

增强list方法

还有一些基于list的增强方法,例如:

 removeRange(int fromIndex, int toIndex)//上锁,判断,拷贝
 ​
 ​
 //查找索引,为-1才上锁
 //如果上锁后发现快照不一致,快照前后数组list一一比较,如果有前后不同且和e相同的,就返回
     public boolean addIfAbsent(E e) {
         Object[] snapshot = getArray();
         return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
             addIfAbsent(e, snapshot);
     }
 ​
  private boolean addIfAbsent(E e, Object[] snapshot) {
         final ReentrantLock lock = this.lock;
         lock.lock();
         try {
             Object[] current = getArray();
             int len = current.length;
             if (snapshot != current) {
                 // Optimize for lost race to another addXXX operation
                 int common = Math.min(snapshot.length, len);
                 for (int i = 0; i < common; i++)
                     if (current[i] != snapshot[i] && eq(e, current[i]))
                         return false;
                 if (indexOf(e, current, common, len) >= 0)
                         return false;
             }
             Object[] newElements = Arrays.copyOf(current, len + 1);
             newElements[len] = e;
             setArray(newElements);
             return true;
         } finally {
             lock.unlock();
         }
     }

Iterator

内部类定义

 static final class COWIterator<E> implements ListIterator<E> {
     /** Snapshot of the array */
     private final Object[] snapshot;
     /** Index of element to be returned by subsequent call to next.  */
     private int cursor;
 ​
     private COWIterator(Object[] elements, int initialCursor) {
         cursor = initialCursor;
         snapshot = elements;
     }
  ...
 }
  • 同样基于数组的特性,提供了往前/往后的遍历功能。

  • 只能做遍历,不支持删除/设置/添加的操作:

     public void remove() {
         throw new UnsupportedOperationException();
     }
     public void set(E e) {
                 throw new UnsupportedOperationException();
             }
     public void add(E e) {
             throw new UnsupportedOperationException();
             }
    

SubList

获取方法

 public List<E> subList(int fromIndex, int toIndex) {
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         Object[] elements = getArray();
         int len = elements.length;
         if (fromIndex < 0 || toIndex > len || fromIndex > toIndex)
             throw new IndexOutOfBoundsException();
         return new COWSubList<E>(this, fromIndex, toIndex);
     } finally {
         lock.unlock();
     }
 }

类定义

 private static class COWSubList<E>
     extends AbstractList<E>
     implements RandomAccess
 {
     private final CopyOnWriteArrayList<E> l;
     private final int offset;
     private int size;
     private Object[] expectedArray;
 ​
     // only call this holding l's lock
     COWSubList(CopyOnWriteArrayList<E> list,
                int fromIndex, int toIndex) {
         l = list;
         expectedArray = l.getArray();
         offset = fromIndex;
         size = toIndex - fromIndex;
     }
     //ArrayList里,是用modCount进行异步修改检查的
     private void checkForComodification() {
             if (l.getArray() != expectedArray)
                 throw new ConcurrentModificationException();
         }
     ...
 }

对subList的操作,简单介绍如下:

  • 无论是CRUD中的哪个种类,操作流程都是:

    • 取父对象的锁并上锁
    • 检查边界,检查异步编辑情况
    • 调用父对象的方法
    • 解锁

线程安全问题

COWArrayList并不能完全避免线程安全问题。

原因:

  • 在get方法中:并不检查index,只是根据当前Array,获取当前Array的某个索引上的数据。

  • remove方法中:copy数组后赋值。

  • 所以存在这种可能:

    • 线程1进入get方法,index = size-1,该位置有值。
    • 线程2进入remove方法,线程1挂起,修改了数组的大小为size-1。
    • 线程2结束,线程1获得时间片继续运行,此时线程1的index值已是非法的值,发生了越界的error。
    • 当然,remove方法比get方法长这么多,同时get方法并不上锁,基本上remove方法很难在get方法挂起时获取的时间片中,完成整个流程,因此这个地方也是理论分析,正常情况下基本是不太可能发生这种事情。
  • 虽然数组是volatile注释的,但是是在两个线程中,volatile并不能保证线程挂起的问题。

  • 但源码中

    • 注释中明确标出get方法会抛出越界异常
    • 并且get方法并没有对边界的检查,因此可能设计时就考虑到了这种情况,只是并没有作为一个bug进行修复,只是作为一个常规的list的特性。