Java中的ArrayList和CopyOnWriteArrayList

ArrayList是List接口的基本实现之一,它是Java Collections Framework的一部分我们可以使用迭代器遍历ArrayList元素。

我们看看一下ArrayList的示例程序:

package com.roin.concurrent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ConcurrentListExample {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");

        // get the iterator
        Iterator<String> it = list.iterator();
        //manipulate list while iterating
        while(it.hasNext()){
            System.out.println("list is:"+list);
            String str = it.next();
            System.out.println(str);
            if(str.equals("B"))
                list.remove("E");
            if(str.equals("C"))
                list.add("C found");
            //below code don't throw ConcurrentModificationException
            //because it doesn't change modCount variable of list
            if(str.equals("D"))
                list.set(1, "D");
        }
    }

}

运行结果:

D:\Java\jdk1.8.0_102\bin\java -Didea.launcher.port=7539 "-Didea.launcher.bin.path=D:\Program Files\JetBrains\IntelliJ IDEA 15.0\bin" -Dfile.encoding=UTF-8 -classpath "D:\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\access-bridge.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Java\jdk1.8.0_102\jre\lib\rt.jar;F:\Javapackage\dubbo\concurrent\out\production\concurrent;D:\Program Files\JetBrains\IntelliJ IDEA 15.0\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain com.roin.concurrent.ConcurrentListExample
list is:[A, B, C, D, E]
A
list is:[A, B, C, D, E]
B
list is:[A, B, C, D]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.roin.concurrent.ConcurrentListExample.main(ConcurrentListExample.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Process finished with exit code 

 们运行上面的程序会报ConcurrentModificationException异常,发生这种情况是因为ArrayList迭代器在设计上快速失效的这意味着一旦创建了迭代器,如果修改了ArrayList,则会抛出ConcurrentModificationException

我们注意到异常是由Iterator next()方法抛出的,现在我们看看源码中 next()里做了什么?

@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];
}

  再看checkForComodification方法里面做了什么?

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

expectedModCount是我们创建迭代器时初始化的迭代器变量,而modCount是保存修改次数。我们每次使用ArrayList的addremovetrimToSize都会修改这个值,当检查到不一致时就会抛出上述异常,因此ArrayList是非安全的。

可以一看一下是在创建迭代器时初始化值的expectedModCount是在创建迭代器时初始化值的

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;

有时我们想要添加或删除列表中的元素,那么我们应该使用并发集合类 - CopyOnWriteArrayList这是java.util.ArrayList的线程安全变体,其中所有可变操作(add,set等)都是通过创建底层数组的新副本来实现的。

CopyOnWriteArrayList为处理引入了额外的性能消耗,但与遍历操作的次数相比,修改的次数很少时,它非常有效。

如果我们把上面程序ArrayList改成CopyOnWriteArrayList,那么我们不会得到任何异常。

现在看运行结果:


我们可以看,list中的元素被改变了,但是迭代的元素还是原来list中的值。

说明:CopyOnWriteArrayList为什么能做到迭代的同时还能进行数据修改,首先根据其名字CopyOnWrite,也就是在写的时候会拷贝一个新的Object数组,然后对新数组进行修改,修改完成后,再将旧数组的引用指向这个新的数组,那么读呢?读还是在旧的数组上读,可以看出往数组里面写和读之间是有时间差的,不是实时的。

我们看看源码中几个关键点:

1.数组是使用volatile修饰的,也就意味着array数组一旦修改,其他线程迭代该数组马上就能拿到值。

private transient volatile Object[] array;

2.读并未加锁,多个线程读操作不会有线程安全问题,多个线程读取会,读取到旧的数据。

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

3.写操作使用了锁,我们看看add方法,其它类似

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

这里加锁的目的是多线程防止拷贝多个数组,我们看到拷贝工作流程,

首先获取原来数组长度,然后拷贝产生一个大小为原来数组+1个元素的新数组,

然后在新数组的最后一个索引位置设置我们要添加的值,

最后把array的引用指向新的数组。

看下setArray方法

/**
 * Sets the array.
 */
final void setArray(Object[] a) {
    array = a;
}

总结一下吧:

ArrayList在迭代的过程中,如果发生增加,删除等导致modCount发生变化的操作时会抛出异常。

CopyOnWriteArrayList,在多线程环境中读多写少场景是比较有效的,它使用写时变更到新数组,然后修改引用指向,读还是在旧数组上读,实现读写分离,写和读有所延迟,数据不保证实时一致性,只能保证最终一致性,另外需要注意这种拷贝对象会耗费不少的内存。









  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值