其实我们日常学习的知识点和问题都是一环扣一环的,很多问题都是相关联的,本次主要围绕着以下问题展开 :
- ArrayList的
remove(Object o)
方法 与Iterator 的remove()
方法有什么区别?(会引出Fail-Fast机制)
- 什么是Fail-Fast机制与Fail-Safe机制机制?
(会引出CopyOnWrite思想)
- 什么是CopyOnWrite思想?
- CopyOnWrite的使用场景与优缺点?
其实我们平时也会遇到这样的需求:将列表中满足某种条件的数据删除;稍后让我们借助下面这个逻辑来简单描述下,就是将列表中将等于"1002"的元素删除,此处就拿遍历ArrayList集合当作事例,代码如下:
private static void testUnsafeArrayList() {
List arr = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arr.add("100"+i);
}
Iterator it = arr.iterator();
while (it.hasNext()) {
String value = String.valueOf(it.next());
if ("1002".equals(value)){
arr.remove(value); //使用这个会抛异常ConcurrentModificationException
// it.remove(); //arr.remove(value)替换成 it.remove()没异常
}
}
System.out.println("删除后的数据:");
Iterator tmp = arr.iterator();
while (tmp.hasNext()) {
String value = String.valueOf(tmp.next());
System.out.println(value);
}
}
运行结果:
执行代码使用 arr.remove(value)
你会发现报错如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.liujc.demo.TestUtil.testUnsafeArrayList(TestUtil.java:69)
at com.liujc.demo.TestUtil.main(TestUtil.java:38)
Process finished with exit code 1
替换方案:
然后使用it.remove()
替换arr.remove(value)
即可正常运行:
这是什么原因呢?
原因是迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,相等的话就返回遍历;否则抛出异常,终止遍历。
这个理由说的似乎有点道理,我要眼见为实。
接下来让我们根据源码来分析下:
首先我们上面代码中利用了ArrayList的remove方法会抛出异常,其实在remove
方法中调用了fastRemove
, 在fastRemove
中执行modCount++;
更新了更改次数,查看源码:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
//此处记修改次数
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
在依次遍历迭代器Iterator 时调用其next()
方法,通过下面next()
源码中可以看到首先调用checkForComodification()
方法,该方法主要判断 expectedModCount
是否与 modCount
相等,不相等抛出异常:
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];
}
final void checkForComodification() {
//此处的modCount已经在remove操作中变更
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
替换方案中使用迭代器的remove方法为什么没有问题?
查看ArrayList的源码中ListItr
继承了Itr
,对应的remove()
中执行expectedModCount = modCount
对expectedModCount
重新赋值绕过后续检查,源码如下:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//此处重新赋值,会绕过后续next()方法中的checkForComodification检查条件
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
其实我们在遇到问题时尽量做到不但要知其然还要知其所以然,要有这种探索精神,但是看源码有时候也要点到为止
。好了,回归正题,上面这个案例分析其实已经描述了Fail-Fast 机制的实现原理,那么现在对Fail-Fast 机制是否了解了呢?
什么是快速失败机制(Fail-Fast)和 安全失败机制(Fail-Safe)?
也许日常没太注意过这两种机制,但是我们肯定都碰到过,其实在我们平时接触的HashMap、ArrayList 这些集合类,这些在 java.util 包的集合类就都是快速失败的(Fail-Fast);而 java.util.concurrent 包下的类都是安全失败(Fail-Safe),比如:CopyOnWriteArrayList等。
Fail-Fast机制
- 什么是Fail-Fast机制?
Fail-fast 机制是 Java 集合中的一种错误机制。
在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出ConcurrentModificationException
异常。 - Fail-fast 机制实现原理
对于实现原理其实根据上面的案例分析更清楚些,这里简单描述下:
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount
变量。集合在被遍历期间如果内容发生变化,就会改变modCount
的值。
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测modCount
变量是否与expectedModCount
值相等,相等的话就返回遍历;否则抛出异常,终止遍历。其实在构造迭代器Iterator
时,通过expectedModCount
记录了集合当前(开始遍历元素之前)的修改次数:private class Itr implements Iterator
{ int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; //首次初始化迭代器时两者赋值相等,两者不等时说明遍历过程集合数据被修改 Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") 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]; } 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(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
Fail-Safe机制
- 什么是Fail-Safe机制?
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛ConcurrentModificationException
异常。 - Fail-Safe机制原理:
在原集合的 copy 上遍历,也就是运用CopyOnWrite思想。 - 缺点:
- 创建原集合的 copy 需要额外的空间和时间上的开销;
- 可能存在脏读的数据,不能保证遍历的是最新的内容。
CopyOnWrite思想(写入时复制)
- CopyOnWrite思想概念
是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明无感知的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。 - CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 - CopyOnWriteArrayList分析
- 概念:
并发版ArrayList,底层结构也是数组,和ArrayList不同之处在于:当新增和删除元素时会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。 - 适用场景:
由于读操作不加锁,写(增、删、改)操作加锁,因此适用于读多写少的场景。 - 局限:
由于读的时候不会加锁(读的效率高,就和普通ArrayList一样),读取的当前副本,因此可能读取到脏数据。
- 概念:
CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
这样做是为了避免在多线程并发add的时候,复制出多个副本出来,可能会出现脏读的情况,导致最终的数组数据不是我们期望的。
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
线程并发的写,则通过锁来控制,如果有线程并发的读,则分以下几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。