• 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就简单说这么多,有不对的地方希望大家及时指出!

  • 相关阅读:
    面试题
    linux I/O复用
    grep
    转载 hadoop 伪分布安装
    hadoop配置文件: hdfs-site.xml, mapred-site.xml
    Format aborted in 格式化namenode 失败的原因
    ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Incompatible namespaceIDs
    /etc/rc.d/init.d/iptables: No such file or directory 错误原因
    linux配置Hadoop伪分布安装模式
    初学Linux 命令
  • 原文地址:https://www.cnblogs.com/1ning/p/6929782.html
Copyright © 2020-2023  润新知