ArrayList并不是线程安全的,在读线程在读取ArrayList的时候如果有写线程在写数据的时候,基于fast-fail机制,会抛出ConcurrentModificationException异常,也就是说ArrayList并不是一个线程安全的容器。那么并发的情况下,这就有了CopyOnWriteArrayList这个东西。
下面主要以下几个方面学习CopyOnWriteArrayList:1)什么是fast-fail机制 2)COW的设计思想 3)add方法实现原理 4)get方法实现原理 5)CopyOnWrite的缺点
fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
COW通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。
通过源码分析可知,CopyOnWriteArrayList使用的数据结构是数组(跟ArrayList一样的)。结构如下
说明:CopyOnWriteArrayList底层使用数组来存放元素。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;//transient是为了不被序列化的
并且该数组引用是被volatile修饰,注意这里仅仅是修饰的是数组引用
那么现在看看add函数,源码如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1. 使用Lock,保证写线程在同一时刻只有一个
lock.lock();
try {
//2. 获取旧数组引用
Object[] elements = getArray();
int len = elements.length;
//3. 创建新的数组,并将旧数组的数据复制到新数组中
Object[] newElements = Arrays.copyOf(elements, len + 1);
//4. 往新数组中添加新的数据
newElements[len] = e;
//5. 将旧数组引用指向新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add方法的逻辑非常好理解:
1、采用ReentrantLock,这样子就把写操作给锁住了,从而就避免的并发。2、数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。3、由于在写数据的时候,是在新的数组中插入数据的,从而保证读写是在两个不同的数据容器中进行操作。
这里没有任何加锁或CAS操作,因为所有的读线程只是会读取数据容器中的数据,并不会进行修改,并没有出现线程不安全的问题,具体看源码如下:
public E get(int index) {
return get(getArray(), index);
}
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
它存在两个问题,即内存占用问题和数据一致性问题
内存占用问题:
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比 如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的minor GC和major GC。
数据一致性问题:
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
多看书,多看底层,多学设计。
https://juejin.im/post/5aeeb55f5188256715478c21 https://www.cnblogs.com/expiator/p/10105419.html http://ifeve.com/java-copy-on-write/