0%

CopyOnWriteArrayList && CopyOnWriteArraySet 总结

这两个都是Java提供的原生写时复制的容器(也就是java.util.concurrent包下的)。CopyOnWrite的基本思路是在修改(包括增加和修改)之前,拷贝出一份快照,对快照进行修改,然后替换原引用。读取操作则没有进行加锁,所以适用于读多写少的场景。另外由于使用了快照模式(迭代器,修改等操作),因此不能保证强一致性,只能保证最终一致性。

CopyOnWriteArraySet是对CopyOnWriteArrayList的包装,所以重点关注一下CopyOnWriteArrayList。

CopyOnWriteArrayList

从增删改查4个方面总结下这个容器(相对ConcurrentHashMap,CopyOnWrite的实现实在是简单太多了。。。)

增加方法就2个:

  • public boolean add(E e)
    • 整体思路比较简单,先获取排他锁(同一时刻只能有一个线程进行修改操作),然后拷贝一份新的数组并插入新的元素(此时读取操作读的还是旧的数组),最后替换引用并释放锁。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      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)
    • 整体流程和上一个方法基本一致,区别在于进行索引的有效判断,同时检测是不是在数组的最后插入,拷贝的过程是调用System.arraycopy进行分区间拷贝。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      public void add(int index, E element) {
      final ReentrantLock lock = this.lock;
      lock.lock();//一样获取独占锁
      try {
      Object[] elements = getArray();//获取当前引用
      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);//直接同上一个方法,生成len+1长度的数组
      else {
      newElements = new Object[len + 1];//生成新的空数组
      System.arraycopy(elements, 0, newElements, 0, index);//拷贝0~index
      System.arraycopy(elements, index, newElements, index + 1,
      numMoved);//拷贝index+1 ~ 最后
      }
      newElements[index] = element;//index位置填充
      setArray(newElements);//替换引用
      } finally {
      lock.unlock();//释放锁
      }
      }

删除操作对外暴露了2个方法,一个是通过索引下标进行删除,一个是找到指定的Object进行删除。

  • public E remove(int index)
    • 这个方法实现和 add(int index, E element) 基本类似,不做过多解释了。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      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));//形成新的0~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)
    • 首先获取数组引用,然后尝试寻找元素,找到了则进入删除操作,找不到则直接返回false
      1
      2
      3
      4
      5
      public boolean remove(Object o) {
      Object[] snapshot = getArray();//获取引用快照
      int index = indexOf(o, snapshot, 0, snapshot.length);//尝试寻找(注意,此时并未加锁,如果此时插入了数据或者其他线程插入了数据,是看不到的,需要在删除的时候重新进行查找)
      return (index < 0) ? false : remove(o, snapshot, index);//找到了则进行删除(index>=0的情况),找不到则返回false
      }
    • 看一下具体的删除操作
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      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])) {//依次遍历查找,找到则结束if语句块,并更新index
      index = i;
      break findIndex;
      }
      }
      if (index >= len)//上述条件并未找到,则判断是否已经超过索引长度(其他线程已经删除元素了,可能会执行到这里)
      return false;
      if (current[index] == o)//这块没想明白什么情况会执行到这里
      break findIndex;
      index = indexOf(o, current, index, len);//重新查找,因为已经加锁了,所以不会有其他线程进行修改
      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();
      }
      }

修改操作相对比较简单,通过索引下标直接更新即可,方法如下:

  • public E set(int index, E element)
    • 操作也不复杂,加锁后重新拷贝并替换,唯独注意就是元素没有变化,也要重新更新一下引用(注释说是为了确保volatile写语义)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      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();
      }
      }

查询操作外部暴露了4个方法,其实内部都是委托给对应的私有方法,只是没有index参数的方法委托的时候传值是0。

  • public int indexOf(Object o)
  • public int indexOf(E e, int index)
  • public int lastIndexOf(Object o)
  • public int lastIndexOf(E e, int index)
    前两个方法委托给 private static int indexOf(Object o, Object[] elements, int index, int fence) 执行
    后两个方法委托给 private static int lastIndexOf(Object o, Object[] elements, int index) 执行
    下面整理一下这两个查找方法:
  • indexOf 只是看一下元素是否为空,为空则找null,不为空则直接进行遍历查找
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private static int indexOf(Object o, Object[] elements,
    int index, int fence) {
    if (o == null) {
    for (int i = index; i < fence; i++)
    if (elements[i] == null)
    return i;
    } else {
    for (int i = index; i < fence; i++)
    if (o.equals(elements[i]))
    return i;
    }
    return -1;
    }
  • lastIndexOf 和 indexOf 几乎相同,只是倒着查找而已。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private static int lastIndexOf(Object o, Object[] elements, int index) {
    if (o == null) {
    for (int i = index; i >= 0; i--)
    if (elements[i] == null)
    return i;
    } else {
    for (int i = index; i >= 0; i--)
    if (o.equals(elements[i]))
    return i;
    }
    return -1;
    }

其他方法

  • contains方法,也是直接调用indexOf方法,判断一下索引位置是不是>=0,就不多说了

  • get方法,直接获取一下数组引用(获取引用之后,其他线程修改,这里也看不到了。。。),然后取对应下标。

  • addIfAbsent方法,CopyOnWriteArraySet就是基于这个方法实现的。先通过indexOf方法查找,找到直接返回false,找不到则加锁进行添加,添加之前会判断一下引用是否改变,和remove比较相似。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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++)//遍历查找,并且找到了,则返回false
    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();
    }
    }
  • containsAll方法, 实现也比较简单暴力,获取快照后,直接for循环对每一个元素进行判断,就不附代码了。

  • removeAll方法,也不算复杂,加锁之后遍历元素,不在待删除集合中,则加入新数组中,然后重新拷贝一份即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public boolean removeAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    if (len != 0) {
    // temp array holds those elements we know we want to keep
    int newlen = 0;
    Object[] temp = new Object[len];
    for (int i = 0; i < len; ++i) {
    Object element = elements[i];
    if (!c.contains(element))
    temp[newlen++] = element;
    }
    if (newlen != len) {
    setArray(Arrays.copyOf(temp, newlen));
    return true;
    }
    }
    return false;
    } finally {
    lock.unlock();
    }
    }
  • retainAll方法,和removeAll方法类似,对应的是两个集合都包含的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public boolean retainAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    if (len != 0) {
    // temp array holds those elements we know we want to keep
    int newlen = 0;
    Object[] temp = new Object[len];
    for (int i = 0; i < len; ++i) {
    Object element = elements[i];
    if (c.contains(element))
    temp[newlen++] = element;
    }
    if (newlen != len) {
    setArray(Arrays.copyOf(temp, newlen));
    return true;
    }
    }
    return false;
    } finally {
    lock.unlock();
    }
    }
  • addAllAbsent方法, indexOf(e, cs, 0, added) 采用将遍历过的待添加元素数组进行替换,节省了新开辟数组的空间,这是一个用的很巧的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public int addAllAbsent(Collection<? extends E> c) {
    Object[] cs = c.toArray();
    if (cs.length == 0)
    return 0;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    int added = 0;
    // uniquify and compact elements in cs
    for (int i = 0; i < cs.length; ++i) {
    Object e = cs[i];
    if (indexOf(e, elements, 0, len) < 0 &&
    indexOf(e, cs, 0, added) < 0)
    cs[added++] = e;
    }
    if (added > 0) {
    Object[] newElements = Arrays.copyOf(elements, len + added);
    System.arraycopy(cs, 0, newElements, len, added);
    setArray(newElements);
    }
    return added;
    } finally {
    lock.unlock();
    }
    }
  • clear方法,就是设置一个空数组,就不附代码了

  • addAll方法,也比较简单,转成数组之后直接合并,带索引位置的,就是跳过一部分后,在插入,最后补充剩余的

  • sort方法,也比较简单,拷贝一份快照后,对快照进行排序,然后替换引用。

  • equals 和 hashCode ,这两个方法都进行了重写,注意一下,都是使用快照进行比较,所以都是弱一致性的。

  • iterator 迭代器是创建一个COWIterator迭代器,不支持删除和修改操作,只能进行读取操作。

  • 其他就不进行分析了

CopyOnWriteArraySet

其实没啥可分析的,内部就一个CopyOnWriteArrayList,所有方法都是调用 CopyOnWriteArrayList的方法,去重用的是addIfAbsent和addAllAbsent方法,= =感觉这个Set性能有点差。。。远不及HashSet和TreeSet。

总结

平时项目中时不时还是会用到CopyOnWriteArrayList,CopyOnWriteArraySet至少我还没用到过,而且看了实现,也不太敢用。。。。
回归主题:

  • CopyOnWrite相关读取操作都是不加锁的,拷贝一份内部数组的引用,就开始读取了,不用加锁的原因就是所有的数组实际上都不会发生改变,因为所有的修改操作都是生成一份新的数组。
  • 因为读取或者迭代器,乃至hashcode,equals方法都是基于快照进行相关操作,所以可能读到的数据并不是最新的,也就是无法保证实时的一致性(就是所谓的弱一致性),但是最终数据还是一致的。
  • 因为几乎每次修改,都会生成新的数组,如果写入比较频繁,可能产生大量垃圾,加重GC负担,所以CopyOnWrite最适合的场景就是读多写少。
  • CopyOnWriteArrayList源码上有很多值得学习的小技巧,比如addAllAbsent方法中用替换数组中读过的位置存储待合并字符,省去了开辟新数组的开销。