不管是在ArrayList、Hashtable还是HashMap的源码中,我们在属性字段都可以看见modCount字段的存在。它用来表示集合修改的次数,例如当执行put操作时就会将modCount加1,都是在其他常用的方法中并没有看到它的具体使用。而它却是fail-fast和fast-safe机制的一种重要参考值,下面就了解一下这两种机制,以及modCount在其中所起到的作用。
并发修改:当一个或多个线程正在遍历一个集合Collection,此时另一个线程修改了这个集合的内容(添加,删除或者修改)。
fail-fast(快速失败)机制指的是当遍历集合的过程中,如果集合的结构发生了改变,例如进行了put操作或是扩容操作,那么程序就会抛出Concurrent Modification Exception。
通常来说,我们在遍历集合的目的就是为了查看集合中存在哪些元素,并不会做其他的骚操作。例如,我们使用iterator来遍历ArrayList:
@Test
public void testIterator(){
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 2,4,5,6,12,38);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
执行单元测试,程序的输出为:
2
4
5
6
12
38
但是如果在遍历的过程中往list中又添加了一个元素呢?如下所示:
@Test
public void testIterator(){
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 2,4,5,6,12,38);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
list.add(100);
System.out.println(iterator.next());
}
}
执行单元测试,程序就会抛出ConcurrentModificationException异常。
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at List.ListDemo.testIterator(ListDemo.java:106)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
这就是fail-fast机制的体现,但是具体在源码中是如何体现的呢?既然它和遍历操作有关,那么我们就需查看相关的源码。ArrayList中有关迭代器的源码如下:
// iterator方法本质上就是返回了一个Itr对象
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
// 游标,用于遍历的过程中取值
int cursor;
// index of last element returned; -1 if no such
int lastRet = -1;
// 这里终于看到了modCount,遍历操作前设置expectedModCount为此时的modCount
int expectedModCount = modCount;
// 无参构造
Itr() {}
// 判断是否还有元素可供遍历
public boolean hasNext() {
// 即判断游标是不是走到了list的最后一个位置
return cursor != size;
}
// 获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 首先就需要检查expectedModCount和modCount是否相等
// 如果不等,说明list结构发生了改变,直接抛ConcurrentModificationException
checkForComodification();
// 根据游标获取值
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
// 如果游标值超过了elementData的长度,说明结构发生了改变,直接抛ConcurrentModificationException
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
// 否则返回游标指向的元素,并更新lastRet的值
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
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
// 只有发生了越界异常才会抛ConcurrentModificationException
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
// 判断iterator中的expectedModCount和当前集合中的modCount值是否相等
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
fail-safe(安全失败)机制是指任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。但是fail-safe机制有两个问题:
@Test
public void test() {
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("Forlogen", 10);
map.put("Kobe", 24);
map.put("James", 23);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext())
{
System.out.println(map.get(iterator.next()));
map.put("Yao", 11);
}
}
fail-safe的具体体现可见java.util.concurrentx中类的实现,例如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。