文章目录
什么是fail-fast机制
fail-fast 是集合中比较常见的错误监测机制,通常出现在集合的遍历之中。
我们首先可看一下例子:
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator iterator = list.listIterator();
while (iterator.hasNext()){
String s = (String) iterator.next();
if("b".equals(s)){
list.remove(s);
}
}
}
}
这段代码竟然执行成功了,不是说,在循环的时候不能删除或者添加元素吗?
我们看下源码:
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
在 ListItr类中维护了一个cursor变量,初始为0。
我们再看hasNext方法
public boolean hasNext() {
return cursor != size;
}
在集合遍历时维护一个初始值为0的游标 cursor,从头到尾进行扫描,当 cursor ==size ,退出遍历。
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
每次变量一个元素,cursor都会加1,
执行remove
这个元素后,所有元素往前拷贝,size变为2,这时cursor 也等于2,再执行hasNext()
时,结果为false,退出循环体,并没有机会执行到next()的第一行代码 checkForComodification()
,此方法用来判断 expectedModCount 和modCount 是否相等,如果不相等,则抛出 ConcurrentModificationException
上面的例子如果list中多一个元素就会出现异常:
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("e");
Iterator iterator = list.listIterator();
while (iterator.hasNext()){
String s = (String) iterator.next();
if("b".equals(s)){
list.remove(s);
}
}
}
}
结果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.yaspeed2.ListTest.main(ListTest.java:16)
那为什么这时候会出现ConcurrentModificationException异常呢?
因为在删除b后 ,如下图
cursor = 2,此时 size =3; 再次执行 next 方法 ,此时会执行 checkForComodification();
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount 是在
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
走到这行代码的时候 Iterator iterator = list.listIterator();
expectedModCount = list.size = 4
但是执行 remove后 modCount 变为3了
再次执行next方法时,expectedModCount != modCount 了 ,所以会报异常!
这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器,即 expectedModCount ,记录已经修改的次数。在进入遍历前,会把实时修改次数modCount赋值给 expectedModCount,如果两个数据不相等,则抛出异常,java.util下的所有集合类都是fail-fast,而concurrent包中的集合类都是fail-safe。
如果想在迭代中删除元素,则可以使用 Iterator进行删除,如果是多线程,则需要加锁
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("e");
Iterator iterator = list.listIterator();
while (iterator.hasNext()){
String s = (String) iterator.next();
synchronized (ListTest.class){
if(s.equals("b")){
iterator.remove();
}
}
}
为什么用Iterator 的remove方法就不会出异常呢? 源码如下,因为
修改后的modCount 又重新赋给了 expectedModCount。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
怎样解决fail-fast
我们可以使用并发容器 CopyOnWriteArrayList 来的代替 ArrayList.
什么是COW呢?
COW 即 Copy-On-Write , 它是并发的一种新思路,实行读写分离。如果是写操作,则复制一个新集合,在新集合内添加或删除元素。当一些修改完成之后,再将原集合的引用指向新的集合。这样做的好处是可以高并发地对COW进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素。
CopyOnWriteArrayList 的 继承关系
- CopyOnWriteArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
- CopyOnWriteArrayList实现了List,提供了基础的添加、删除、遍历等操作。
- CopyOnWriteArrayList实现了RandomAccess,提供了随机访问的能力。
- CopyOnWriteArrayList实现了Cloneable,可以被克隆。
- CopyOnWriteArrayList实现了Serializable,可以被序列化。
CopyOnWriteArrayList 源码分析
属性
/** The lock protecting all mutators */
// 修改时需要加锁
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
// 真正存储元素的地方
private transient volatile Object[] array;
方法
add(E e) 方法
添加一个元素到末尾
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();
}
}
执行步骤如下:
- 加锁
- 获取旧的数组
- 新建一个数组,大小为原来长度+1, 并把旧的数据拷贝进去
- 添加元素到新数组
- 把旧数组的引用指向新数组
add(int index,E element) 方法
在指定你索引处添加元素
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 获取旧数组
Object[] elements = getArray();
int len = elements.length;
// 检查是否越界, 可以等于len
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
// 如果插入的位置是最后一位
// 那么拷贝一个n+1的数组, 其前n个元素与旧数组一致
newElements = Arrays.copyOf(elements, len + 1);
else {
// 如果插入的位置不是最后一位
// 那么新建一个n+1的数组
newElements = new Object[len + 1];
// 拷贝旧数组前index的元素到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
// 将index及其之后的元素往后挪一位拷贝到新数组中
// 这样正好index位置是空出来的
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
// 将元素放置在index处
newElements[index] = element;
setArray(newElements);
} finally {
// 释放锁
lock.unlock();
}
}
get(int index)
获取指定的元素,支持随机访问,时间复杂度为O(1)
public E get(int index) {
// 获取元素不需要加锁
// 直接返回index位置的元素
// 这里是没有做越界检查的, 因为数组本身会做越界检查
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
从上面源码可以看到,读取操作是不需要加锁的。
CopyOnWrite的应用场景
CopyOnWrite 并发容器用于读多谢少的并发场景。比如白名单,黑名单等场景。
CopyOnWrite的缺点
1 内存占用问题: 因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
- ** 数据一致性问题**。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
总结
(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全;
(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性;