先来了解一下Copy-On-Write(写时复制技术):通俗的讲,写时复制技术就是不同进程访问同一资源的时候,只有在写操作,才会去复制一份新的数据,否则都是访问同一个资源。
CopyOnWriteArrayList,是一个写入时复制的容器,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。
那么java里面是如何实现的,看源码:
/** * Appends the specified element to the end of this list.【添加元素至列表末尾】 * @param e element to be appended to this list 【e:新增的元素】 * @return {@code true} (as specified by {@link Collection#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); //拷贝一份旧容器,并且扩容+1 newElements[len] = e; //填充元素 setArray(newElements); //新的集合列表替换掉旧的 return true; } finally { lock.unlock(); } }我们看到Java中JDK的源码实现其实也是非常的简单,往一个容器里添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行cpoy,复制出来一个新的容器object[] newElements,然后往新的容器newElements里面添加元素。添加完成之后再将原容器的引用指向新的容器setArray(newElements);。
优点:对于一些读多写少的数据,这种做法的确很不错,对容器并发的读不需要加锁,因为此时容器内不会添加任何新的元素。所以CopyOnWriteArrayList也是一种读写分离的思想,读和写操作的是不同的容器。
缺点:这种实现只保证数据的最终一致性,在副本未替换掉旧数据时,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发频繁的GC,此时,应考虑其他的容器,例如ConcurrentHashMap。