ArrayList和CopyOnWriteArrayList

前言

  在开发过程中需要程序猿考虑线程安全的情况,java中ArrayList是线程不安全,对应的线程安全类是CopyOnWriteArrayList类,这里简单说一下这两个类。

1、ArrayList的遍历和fail-fast

  对于集合的遍历通常有三种方式:for循环、foreach语法糖和Iterator迭代器,其实后两者是一样的,只不过写法不同而已,foreach其实也是使用了迭代器来实现的。我们经常有这样的需求,在遍历过程中需要对集合的元素进行增加或修改操作,这里先贴上一个错误的方式代码

 1     List<String> list = new ArrayList<>();
 2     list.add("a");
 3     list.add("b");
 4     list.add("c");
 5     list.add("d");
 6     for (int i = 0;i < list.size(); i++ ) {        
 7         if("b".equals(list.get(i))){
 8             list.remove(i);
 9     }else{
10             System.out.println(list.get(i));//or do something
11         }    
12     }                    

  我们预期的打印结果是a、c、d,但实际结果却是a、d,元素c丢失了。造成这个问题的原因就是,当list遍历到第二个元素b的时候,调用remove方法,这时候list的结构发生了变化,长度减少了1,而此时i的值依然从1自增到了2,所以下一次打印结果就是get(2)。在遍历集合的时候对集合进行操作时不推荐使用传统的for循环方式,接下来我们再贴上foreach语法糖的代码

1      for (String str : list) {
2         if("b".equals(str)){
3             list.remove(str);
4         }else{
5             System.out.println(str);
6         }
7     }

  运行一下发现报错了,抛出了这个异常java.util.ConcurrentModificationException,这个是ArrayList的fail-fast机制。那什么是fail-fast呢?我们知道ArrayList是线程不安全的,换句话说,如果有两个线程,一个线程对ArrayList遍历,另一个线程对ArrayList进行修改操作,那ArrayList会尽力抛出异常,告诉我们同时操作是不允许的。我们看一下ArrayList是如何抛出这个异常的,前面说过,foreach语法糖其实就是Iterator迭代器,我们看到在ArrayList类中有个内部类Itr,这个类中有个变量叫expectedModCount,它的初始值等于ArrayList类中的modCount,当调用迭代器中的next()方法时会比较这两个变量的值是否相等,如果不相等则抛出上述的异常。ArrayList每次的修改操作都会对modCount进行自增操作,所以每次调用remove()方法,modCount值都会发生变化,导致与expectedModCount不相等,从而抛出异常。

 1     public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // Let gc do its work
12 
13         return oldValue;
14     }

  这个是ArrayList的remove方法,这里的modCount进行了自增的操作。

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

  这个是内部类Itr的next()方法,里面调用了checkForComodification()方法,这个方法就是校验modCount和expectedModCount是否相等。

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

  那么对于上述的需求我们到底应该怎么做呢,这里推荐大家使用Iterator迭代器来实现,在迭代过程中实现元素的remove操作

1     Iterator<String> it = list.iterator();
2     while(it.hasNext()){
3         String str = it.next();
4         if("b".equals(str)){
5             it.remove();
6         }else{
7             System.out.println(str);
8         }
9     }

  我们发现这是可以的,而且结果也是正确的,理由就是在Itr中的remove()方法会将modCount和expectedModCount设置相等。

2、CopyOnWriteArrayList

  上面说了fail-fast,这里简单说一说fail-safe。对于CopyOnWriteArrayList来说,一个线程遍历它,一个线程修改它的元素时不会出现上面的问题,fail-safe的原理就是每次对元素的修改都会在副本上进行,也就是说CopyOnWriteArrayList对于set、remove、add等请求会先将集合做一个复制,然后所有的增删改操作均在复制的集合上操作,然后再将结果写回来。我们看一下CopyOnWriteArrayList的源码

1     private volatile transient Object[] array;

  在CopyOnWriteArrayList中这样一个属性array,它是真正存储数据的地方,这里再看一下add()方法的代码

 1     public boolean add(E e) {
 2         final ReentrantLock lock = this.lock;
 3         lock.lock();
 4         try {
 5             Object[] elements = getArray();
 6             int len = elements.length;
 7             Object[] newElements = Arrays.copyOf(elements, len + 1);
 8             newElements[len] = e;
 9             setArray(newElements);
10             return true;
11         } finally {
12             lock.unlock();
13         }
14     }

  先进行加锁操作,然后获取数组,其实就是获取array属性的值,复制一份进行新增操作,再写回array属性,最后释放锁。其实我们从名字就可以猜测出来其原理,copyonwrite在redis持久化fork子进程时也有类似的应用。关于CopyOnWriteArrayList就简单说这么多,有不对的地方希望大家及时指出!

posted @ 2017-06-01 17:07  _Emotion丶小寳  阅读(196)  评论(0编辑  收藏  举报