java基础提高之CopyOnWriteArrayList

316 阅读4分钟

        这一篇系集合list类最后一篇,这次我们来一起学习一下CopyOnWriteArrayList。

简单介绍

        老规矩,先简单介绍一下。在源码中,开头注释是这么写的,ArrayList的一个线程安全的变种,其中所有的可变操作包括add、set等都是通过底层数组的副本实现的。

        Copy-On-Write,在写入时复制,这个就说明了这个类的工作方式,在进行可变操作比如add、set等操作时先复制一份源的副本,对副本进行操作,然后再将源列表的引用指向副本。

        这么做其实成本很高,但是当改变的操作(add、set、remove等)远远少于遍历的操作的时候,这个CopyOnWriteArrayList可能更有效率。并且在不能或不想对遍历加锁,但是想要排除并发线程的干扰时也是非常有用的。快照方式的iterator用了一个指向这个iterator被创建时数组的状态的引用。这个状态的引用在iterator的声明周期中永远也不会改变,所以干扰时不可能的,并且iterator保证不会抛出ConcurrentModificationException。迭代器(iterator)创建以后,对原列表的增加、删除或者改变都不会反映到这个iterator上。iterator即这个迭代器不支持remove、set、add等改变元素的操作,调用这些方法会抛出UnsupportedOperationException。

        支持所有类型的元素,包括null。

        内存一致性效果:同其他并发集合一样,将对象放入CopyOnWriteArrayList之前的线程的操作,先于另一个线程从CopyOnWriteArrayList中删除或者访问元素后的操作发生。

基本操作

        我们也来首先看一下内部存储结构:

  1. lock: ReentrantLoct,这个锁会在以后进行介绍,现在我们知道他是保护所有可变操作的锁就行了。
  2. array: 这个就是CopyOnWriteArrayList的存储结构了,我们可以看出就是一个数组。

接下来我们根据add这个方法来看一下这个类的主要实现方式,

这么一看其实很简单,在添加元素之前首先获得锁,之所以加锁,是为了多线程环境下,防止复制出多个副本来。之后使用getArray()方法去获得原本的元素列表,然后根据Array.copyOf方法区根据需要去复制一个新的数组,这里是将新数组的长度加一。之后对这个新数组进行操作。 最后用setArray方法将这个CopyOnWriteArrayList内部存储数组指向这个新数组。然后使用lock.unlock()释放锁。我们来看一下getArray()方法与setArray方法。

没啥就是一个get与set,但是是final的即不可被重写。

总结

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  **内存占用问题。**因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。

  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

  **数据一致性问题。**CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

参考:www.cnblogs.com/dolphin0520…