由ArrayList来深入理解Java中的fail-fast机制
1. fail-fast简介
“快速失败”也就是fail-fast,它是java集合的一种错误检测机制。某个线程在对collection进行迭代时,不允许其他线程对该collection进行结构上的修改。
例如:假设存在两个线程(线程1、线程2),线程1通过iterator在遍历集合a中的元素,在某个时候线程2修改了集合a的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 concurrentmodificationexception 异常,从而产生fail-fast。
迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,因此,concurrentmodificationexception应该仅用于检测 bug。
java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;
快速失败的迭代器抛出concurrentmodificationexception,而安全失败的迭代器从不抛出这个异常。
2 fail-fast示例
示例代码:(fastfailtest.java)
import java.util.*; import java.util.concurrent.*; /* * @desc java集合中fast-fail的测试程序。 * * fast-fail事件产生的条件:当多个线程对collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出concurrentmodificationexception异常。 * fast-fail解决办法:通过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。 * * 本例中,分别测试arraylist和copyonwritearraylist这两种情况。arraylist会产生fast-fail事件,而copyonwritearraylist不会产生fast-fail事件。 * (01) 使用arraylist时,会产生fast-fail事件,抛出concurrentmodificationexception异常;定义如下: * private static list<string> list = new arraylist<string>(); * (02) 使用时copyonwritearraylist,不会产生fast-fail事件;定义如下: * private static list<string> list = new copyonwritearraylist<string>(); * * @author skywang */ public class fastfailtest { private static list<string> list = new arraylist<string>(); //private static list<string> list = new copyonwritearraylist<string>(); public static void main(string[] args) { // 同时启动两个线程对list进行操作! new threadone().start(); new threadtwo().start(); } private static void printall() { system.out.println(""); string value = null; iterator iter = list.iterator(); while(iter.hasnext()) { value = (string)iter.next(); system.out.print(value+", "); } } /** * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printall()遍历整个list */ private static class threadone extends thread { public void run() { int i = 0; while (i<6) { list.add(string.valueof(i)); printall(); i++; } } } /** * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printall()遍历整个list */ private static class threadtwo extends thread { public void run() { int i = 10; while (i<16) { list.add(string.valueof(i)); printall(); i++; } } } }
运行结果
运行该代码,抛出异常java.util.concurrentmodificationexception!即,产生fail-fast事件!
结果说明
(01) fastfailtest中通过 new threadone().start() 和 new threadtwo().start() 同时启动两个线程去操作list。
threadone线程:向list中依次添加0,1,2,3,4,5。每添加一个数之后,就通过printall()遍历整个list。
threadtwo线程:向list中依次添加10,11,12,13,14,15。每添加一个数之后,就通过printall()遍历整个list。
(02) 当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出concurrentmodificationexception异常,产生fail-fast事件。
3. fail-fast解决办法
fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为jdk并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
所以,本例中只需要将arraylist替换成java.util.concurrent包下对应的类即可。 即,将代码
private static list<string> list = new arraylist<string>();
替换为
private static list<string> list = new copyonwritearraylist<string>();
则可以解决该办法。
4. fail-fast原理
产生fail-fast事件,是通过抛出concurrentmodificationexception异常来触发的。
那么,arraylist是如何抛出concurrentmodificationexception异常的呢?
我们知道,concurrentmodificationexception是在操作iterator时抛出的异常。我们先看看iterator的源码。arraylist的iterator是在父类abstractlist.java中实现的。代码如下:
package java.util;
public abstract class abstractlist<e> extends abstractcollection<e> implements list<e> { ... // abstractlist中唯一的属性 // 用来记录list修改的次数:每修改一次(添加/删除等操作),将modcount+1 protected transient int modcount = 0; // 返回list对应迭代器。实际上,是返回itr对象。 public iterator<e> iterator() { return new itr(); } // itr是iterator(迭代器)的实现类 private class itr implements iterator<e> { int cursor = 0; int lastret = -1; // 修改数的记录值。 // 每次新建itr()对象时,都会保存新建该对象时对应的modcount; // 以后每次遍历list中的元素的时候,都会比较expectedmodcount和modcount是否相等; // 若不相等,则抛出concurrentmodificationexception异常,产生fail-fast事件。 int expectedmodcount = modcount; public boolean hasnext() { return cursor != size(); } public e next() { // 获取下一个元素之前,都会判断“新建itr对象时保存的modcount”和“当前的modcount”是否相等; // 若不相等,则抛出concurrentmodificationexception异常,产生fail-fast事件。 checkforcomodification(); try { e next = get(cursor); lastret = cursor++; return next; } catch (indexoutofboundsexception e) { checkforcomodification(); throw new nosuchelementexception(); } } public void remove() { if (lastret == -1) throw new illegalstateexception(); checkforcomodification(); try { abstractlist.this.remove(lastret); if (lastret < cursor) cursor--; lastret = -1; expectedmodcount = modcount; } catch (indexoutofboundsexception e) { throw new concurrentmodificationexception(); } } final void checkforcomodification() { if (modcount != expectedmodcount) throw new concurrentmodificationexception(); } } ... }
从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkforcomodification()。若 “modcount 不等于 expectedmodcount”,则抛出concurrentmodificationexception异常,产生fail-fast事件。
要搞明白 fail-fast机制,我们就要需要理解什么时候“modcount 不等于 expectedmodcount”!
从itr类中,我们知道 expectedmodcount 在创建itr对象时,被赋值为 modcount。通过itr,我们知道:expectedmodcount不可能被修改为不等于 modcount。所以,需要考证的就是modcount何时会被修改。
接下来,我们查看arraylist的源码,来看看modcount是如何被修改的。
package java.util; public class arraylist<e> extends abstractlist<e> implements list<e>, randomaccess, cloneable, java.io.serializable { ... // list中容量变化时,对应的同步函数 public void ensurecapacity(int mincapacity) { modcount++; int oldcapacity = elementdata.length; if (mincapacity > oldcapacity) { object olddata[] = elementdata; int newcapacity = (oldcapacity * 3)/2 + 1; if (newcapacity < mincapacity) newcapacity = mincapacity; // mincapacity is usually close to size, so this is a win: elementdata = arrays.copyof(elementdata, newcapacity); } } // 添加元素到队列最后 public boolean add(e e) { // 修改modcount ensurecapacity(size + 1); // increments modcount!! elementdata[size++] = e; return true; } // 添加元素到指定的位置 public void add(int index, e element) { if (index > size || index < 0) throw new indexoutofboundsexception( "index: "+index+", size: "+size); // 修改modcount ensurecapacity(size+1); // increments modcount!! system.arraycopy(elementdata, index, elementdata, index + 1, size - index); elementdata[index] = element; size++; } // 添加集合 public boolean addall(collection<? extends e> c) { object[] a = c.toarray(); int numnew = a.length; // 修改modcount ensurecapacity(size + numnew); // increments modcount system.arraycopy(a, 0, elementdata, size, numnew); size += numnew; return numnew != 0; } // 删除指定位置的元素 public e remove(int index) { rangecheck(index); // 修改modcount modcount++; e oldvalue = (e) elementdata[index]; int nummoved = size - index - 1; if (nummoved > 0) system.arraycopy(elementdata, index+1, elementdata, index, nummoved); elementdata[--size] = null; // let gc do its work return oldvalue; } // 快速删除指定位置的元素 private void fastremove(int index) { // 修改modcount modcount++; int nummoved = size - index - 1; if (nummoved > 0) system.arraycopy(elementdata, index+1, elementdata, index, nummoved); elementdata[--size] = null; // let gc do its work } // 清空集合 public void clear() { // 修改modcount modcount++; // let gc do its work for (int i = 0; i < size; i++) elementdata[i] = null; size = 0; } ... }
从中,我们发现:无论是add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modcount的值。
接下来,我们再系统的梳理一下fail-fast是怎么产生的。步骤如下:
(01) 新建了一个arraylist,名称为arraylist。
(02) 向arraylist中添加内容。
(03) 新建一个“线程a”,并在“线程a”中通过iterator反复的读取arraylist的值。
(04) 新建一个“线程b”,在“线程b”中删除arraylist中的一个“节点a”。
(05) 这时,就会产生有趣的事件了。
在某一时刻,“线程a”创建了arraylist的iterator。此时“节点a”仍然存在于arraylist中,创建arraylist时,expectedmodcount = modcount(假设它们此时的值为n)。
在“线程a”在遍历arraylist过程中的某一时刻,“线程b”执行了,并且“线程b”删除了arraylist中的“节点a”。“线程b”执行remove()进行删除操作时,在remove()中执行了“modcount++”,此时modcount变成了n+1!
“线程a”接着遍历,当它执行到next()函数时,调用checkforcomodification()比较“expectedmodcount”和“modcount”的大小;而“expectedmodcount=n”,“modcount=n+1”,这样,便抛出concurrentmodificationexception异常,产生fail-fast事件。
至此,我们就完全了解了fail-fast是如何产生的!
即,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modcount的值);这时,就会抛出concurrentmodificationexception异常,产生fail-fast事件。
5. 解决fail-fast的原理
上面,说明了“解决fail-fast机制的办法”,也知道了“fail-fast产生的根本原因”。接下来,我们再进一步谈谈java.util.concurrent包中是如何解决fail-fast事件的。
还是以和arraylist对应的copyonwritearraylist进行说明。我们先看看copyonwritearraylist的源码:
package java.util.concurrent; import java.util.*; import java.util.concurrent.locks.*; import sun.misc.unsafe; public class copyonwritearraylist<e> implements list<e>, randomaccess, cloneable, java.io.serializable { ... // 返回集合对应的迭代器 public iterator<e> iterator() { return new集合类中的fast-fail实现方式都差不多,我们以最简单的arraylist为例吧。protected transient int modcount = 0;记录的是我们对arraylist修改的次数,比如我们调用 add(),remove()等改变数据的操作时,会将modcount++。protected transient int modcount = 0;记录的是我们对arraylist修改的次数,比如我们调用 add(),remove()等改变数据的操作时,会将modcount++。 cowiterator<e>(getarray(), 0); } ... private static class cowiterator<e> implements listiterator<e> { private final object[] snapshot; private int cursor; private cowiterator(object[] elements, int initialcursor) { cursor = initialcursor; // 新建cowiterator时,将集合中的元素保存到一个新的拷贝数组中。 // 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。 snapshot = elements; } public boolean hasnext() { return cursor < snapshot.length; } public boolean hasprevious() { return cursor > 0; } public e next() { if (! hasnext()) throw new nosuchelementexception(); return (e) snapshot[cursor++]; } public e previous() { if (! hasprevious()) throw new nosuchelementexception(); return (e) snapshot[--cursor]; } public int nextindex() { return cursor; } public int previousindex() { return cursor-1; } public void remove() { throw new unsupportedoperationexception(); } public void set(e e) { throw new unsupportedoperationexception(); } public void add(e e) { throw new unsupportedoperationexception(); } } ... }
从中,我们可以看出:
(01) 和arraylist继承于abstractlist不同,copyonwritearraylist没有继承于abstractlist,它仅仅只是实现了list接口。
(02) arraylist的iterator()函数返回的iterator是在abstractlist中实现的;而copyonwritearraylist是自己实现iterator。
(03) arraylist的iterator实现类中调用next()时,会“调用checkforcomodification()比较‘expectedmodcount'和‘modcount'的大小”;但是,copyonwritearraylist的iterator实现类中,没有所谓的checkforcomodification(),更不会抛出concurrentmodificationexception异常!
6. 总结
由于hashmap(arraylist)并不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map(这里的修改是指结构上的修改,并非指单纯修改集合内容的元素),那么将要抛出concurrentmodificationexception 即为fail-fast策略
主要通过modcount域来实现,保证线程之间的可见性,modcount即为修改次数,对于hashmap(arraylist)内容的修改就会增加这个值, 那么在迭代器的初始化过程中就会将这个值赋值给迭代器的expectedmodcount
但是fail-fast行为并不能保证,因此依赖于此异常的程序的做法是错误的