在使用集合时候,大家应该都遇到过或听过并发修改异常(ConcurrentModificationException),这其实是Java集合中的一种fail-fast机制,为了避免触发fail-fast机制,Java中还提供了一些采用fail-safe机制设计的集合类,本篇文章就系统的介绍一下这两种机制。
简单的说,这就是系统设计的一种理念,就是在编码的时候优先考虑异常情况,一旦发生异常,就立即终止,并上报(抛异常),比如最经典的除0异常:
public int divide(int i,int j){
if(j==0){
throw new RuntimeException("被除数不能为零");
}
return i/j;
}
上述就是一个fail-fast的理念,快速失败,不做无用功。
fail-fast机制是Java集合的一种并发检测机制,在进行foreach遍历时,都会提取保存modCount的值,然后每执行一次遍历add/remove操作前会进行比较,如果不一致就会抛出ConcurrentModificationException异常。
ps:除了JUC目录下的并发集合,Collection中所有Iterator的实现类(比如ArrayList、HashMap等)都是fail-fast的设计。
下面以ArrayList为例,来看下效果:
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+"");
}
System.out.println("list = " + list);
for (String str : list) {
//list.add("hello"); 同样会抛异常
list.remove(str);
}
}
这是什么原因造成的呢?不要着急,在深入原理之前,我们先看下这个增强for的语法糖具体时如何实现的,我们使用jad工具,对编译后的class进行反编译,就会得到如下代码:
public static void main(String args[])
{
List list = new ArrayList();
for(int i = 0; i < 10; i++)
list.add((new StringBuilder()).append(i).append("").toString());
String str;
for(Iterator iterator = list.iterator(); iterator.hasNext(); list.remove(str))
str = (String)iterator.next();
}
不难发现,增强for是Iterator实现的,通过报错信息我们也可以定位到是在checkForComodification()方法中,该方法是在iterator.next()方法中调用的源码如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount我们并不陌生,无论是在HashMap还是ArrayList中都可以看到,它表示的是集合实际被修改的次数,而expectedModCount则是ArrayList中的一个内部类——Itr中的成员变量,部分源码如下:
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;
//...
}
该类实现了Iterator接口,expectedModCount表示的是这个迭代器预期该集合被修改的次数,该值随着Itr被创建才会初始化,expectedModCount = modCount。
这下就真相大白了!上述测试代码在增强for遍历之前,modCount和expectedModCount的值都是10,第一轮遍历,可以正常remove,因为此时大家都是10。第一轮遍历之后,由于调用的是ArrayList的remove方法,modCount就变为了11,此时expectedModCount还是10,所以在进行第二轮遍历的时候,由于它俩不相等了,所以就发生了ConcurrentModificationException异常。
为了避免触发fail-fast机制,我们可以使用JUC包下的集合类,该包下的集合都是fail-safe的,在遍历的时候不是直接在原集合上访问的,而是先copy一份原集合的副本,然后在副本的基础上的再进行遍历,下面我们用CopyOnWriteArrayList举例:
public static void main(String[] args){
List list=new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+"");
}
for (String str : list) {
list.remove(str);
}
System.out.println("list = " + list);
}
fail-safe集合的所有集合都是先拷贝一份副本,然后在副本上进行操作的,而且这些操作(add/remove)都是通过锁来控制并发的,所以在迭代的过程中不需要fail-fast的并发检测。
有同学可能有疑问,上面不是说了吗,用JUC下的集合类,当然这只是其中的一个避免方式,再说JUC下的都是线程安全的,而我们实际场景中还是非线程安全集合用的比较多,那么它们如何避免呢?
细心的同学可能会注意到我在fail-fast代码的例子中,用的是fori(普通集合遍历)来初始化list的,它并没有出现并发修改的异常,因为它本质上使用的ArrayList的remove方法,该方法不会进行并发检测。除此之外,还有几种方式也可以避免fail-fast机制。
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+"");
}
System.out.println("list = " + list);
for (int i = 0; i < list.size(); i++) {
list.remove(i);
i--;
}
System.out.println("list = " + list);
}
以下结果如果是这个,就不再赘述了:
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = []
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+"");
}
System.out.println("list = " + list);
Iterator itr = list.iterator();
while (itr.hasNext()){
itr.next();
//这里用迭代器就没啥问题了
itr.remove();
}
System.out.println("list = " + list);
}
我们可以看下迭代器remove的源码(ArrayList中Itr内部类的方法):
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();
}
}
将原来的copy出一个副本,注意要遍历原来的list,然后进行remove:
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+"");
}
System.out.println("list = " + list);
List list1=new ArrayList<>(list);
for (String s : list) {
list1.remove(s);
}
System.out.println("list = " + list1);
}
通过Stream的filter方法进行remove,因为Stream每次处理后都会生成一个新的Stream,不存在并发问题:
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println("list = " + list);
List integers = list.stream().filter(i -> i < 5).collect(Collectors.toList());
System.out.println("list = " + integers);
}
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = [0, 1, 2, 3, 4]
也是过滤删除,jdk 1.8开始,List提供了一个removeIf方法用于删除所有满足特定条件的元素:
public static void main(String[] args) {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println("list = " + list);
list.removeIf(i->i>5);
System.out.println("list = " + list);
}
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = [0, 1, 2, 3, 4, 5]
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教。