1.引入
遍历一个集合,我们有三种办法:
- for循环
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
- foreach
for (int i : list) {
System.out.println(i);
}
- 迭代器Iterator
while (iterator.hasNext()){
int next = (int) iterator.next();
System.out.println(next);
}
单线程情况下这三种遍历都能正常运行,而迭代器的方式在数据量大的情况下是很快的。
看一下Iterator的源码
public interface Iterator<E> {
//如果迭代有更多元素,则返回true
boolean hasNext();
//返回迭代中的下一个元素
E next();
//从底层集合中移除此迭代器返回的最后一个元素(可选操作)。
每次调用next只能调用此方法一次。 如果在迭代正在进行时以除调用此方法
以外的任何方式修改了基础集合,则迭代器的行为是未指定的
default void remove() {
throw new UnsupportedOperationException("remove");
}
//对每个剩余元素执行给定的操作,直到处理完所有元素或操作引发异常。
如果指定了该顺序,则操作按迭代顺序执行。
动作抛出的异常被转发给调用者。
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
我们通常使用的只有hasNext()方法和next()方法,如果我们在迭代时调用remove会怎样呢?
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
int next = (int) iterator.next();
iterator.remove();
}
System.out.println(list.size());;
}
可以看到ArrayList里面的元素被删除了,源码中提出“以此方式以外的修改集合元素则迭代器的行为不确定",那我们来试一下用另一个线程迭代时改动。
private static List<Integer> list = new ArrayList<>();
private static class thread1 extends Thread {
@SneakyThrows
@Override
public void run() {
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
System.out.println("thread1遍历:" + i);
Thread.sleep(100);
}
}
}
private static class thread2 extends Thread {
@SneakyThrows
@Override
public void run() {
Thread.sleep(200);
list.remove(9);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
new thread1().start();
new thread2().start();
}
线程1遍历时让线程2去修改列表元素。
我们发现报了并发修改异常,因此可以初步了解fail-fast,就是程序对集合进行迭代时,其它线程对集合进行了结构的修改(比如删除、添加元素)。当检测到对象的并发修改,但只是抛出该异常并不能阻止,有的时候甚至不报异常,所以我们只能把ConcurrentModificationException当做一个信息,不能针对只报了这个异常就只去检查多线程的问题,其它情况也可能会造成集合元素修改。
2.分析
以ArrayList为例,我们看看它如何实现Iterator的
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;
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<? super E> 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();
}
}
我们发现这些方法里面都调用了checkForComodification();
而这个方法里面只有一个if (modCount != expectedModCount)
所以上面为什么并发修改异常的关键就在这里了。
expectedModCount 是在Itr中定义的:
int expectedModCount = ArrayList.this.modCount;
Itr里面的所有方法都未涉及expectedModCount的修改,所以他的值是不可能会变的,那么会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:
protected transient int modCount = 0;
那它什么时候会改变呢?来接着往下看源码
public boolean add(E paramE) {
ensureCapacityInternal(this.size + 1);
/** 此处省略代码 */
}
private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1;
/** 此处省略代码 */
}
public boolean remove(Object paramObject) {
int i;
if (paramObject == null)
for (i = 0; i < this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else
for (i = 0; i < this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i);
return true;
}
return false;
}
private void fastRemove(int paramInt) {
this.modCount += 1;
/** 此处省略代码 */
}
public void clear() {
this.modCount += 1;
/** 此处省略代码 */
}
因此我们就明白了,集合的结构改变会引起modCount改变从而导致迭代时产生异常。所以fail-fast的原理就出来了:
当使用迭代器遍历集合时,遍历期间对集合本身的结构的修改会导致modCount增大,和expectedModCount 不一致导致报出ConcurrentModificationException
3.解决
目前我了解的有以下两种方案:
- 对涉及modCount的地加上synchronized锁或者直接使用 Collection synchronizedList,但是对于加锁操作不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
- 使用CopyOnWriteArrayList 替换 ArrayLIst(fail-safe机制),对于map可以使用CurrentHashMap(fail-safe机制)
根据名字也可以看出,CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是先对底层数组复制一次,然后对复制数组进行操作,这样每个线程操作自己复制的那一份数组,自然就不会产生冲突了。 但是由于需要复制数组所以该类产生的开销比较大,比较适用下面两种情况:
-
在不能同步遍历,但又需要从并发线程中排除冲突时。
-
当遍历操作的数量大大超过可变操作的数量时。
4.fail-safe 机制
1、fail-safe 任何对集合结构的修改都会在一个复制后的新集合上进行,因此不会产生冲突。
2、fail-safe 机制有两个缺点:
(1)需要复制集合,产生大量的无效对象,开销大。
(2)无法保证读取到的数据是目前原始结构中的数据。
3、使用示例:
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
list.add(4);
}
System.out.println(list.size());
}
我们可以看到遍历只输出了1 2 3,而4添加三次后size变成6
这两个机制主要是在hashmap可能会问到,hashtable也是fail-fast的,大家可以再去了解一下。
最后
快速失败其实就是失败后程序抛异常并且停止运行,安全失败就是可以继续运行。他们是一个思想,而不局限于使用在集合中,比如dubbo的集群容错也用到了,如果快速失败就抛异常停止运行,安全失败就打印日志,返回空数据。