• 在ConcurrentModificationException异常上的联想


    1.什么是ConcurrentModificationException?

    大家都听说过快速报错fast-fail吧,fast-fail的发生就是说明发生了ConcurrentModificationException异常。其实发生这种异常的事件有两种,一种是在Iterator在迭代过程中,出现删除或者增加Collection中的元素的时候。另一种是多线程情况下,一个线程在用Iterator迭代Collection元素时,另一个线程却在给这个Collection增加或者删除元素。大家也许看出来了,两种情况其实都是在Iterator迭代元素过程中增加或者删除元素。

    2.制造ConcurrentModificationException?

    /**
     * Cme表示ConcurrentModificationException
     * 目的是制造ConcurrentModificationException
     */
    public class Cme implements Runnable{
        List list = new ArrayList();
    
        @Override
        public void run() {
            list.add(1);
            list.add(2);
    
            Iterator itr = list.listIterator();
            while (itr.hasNext()) {
                System.out.println(itr.next());
                list.add(3);
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Cme cm = new Cme();
            Thread thread_1 = new Thread(cm);
            thread_1.start();
    
        }
    }

    运行结果:

    1
    Exception in thread "Thread-0" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
        at java.util.ArrayList$Itr.next(ArrayList.java:851)
        at dp.Cme.run(Cme.java:17)
        at java.lang.Thread.run(Thread.java:745)

    确实是发生错误了,点击错误信息,调到了下面的代码:

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

    大概意思是Collection中的元素与开始遍历的时候传来的个数不相同。

    那到底是什么调用了上面的方法?

    public E next() {
               checkForComodification();
               int i = cursor;
               if (i >= SubList.this.size)
                      throw new NoSuchElementException();
               Object[] elementData = ArrayList.this.elementData;
               if (offset + i >= elementData.length)
                            throw new ConcurrentModificationException();
               cursor = i + 1;
                     return (E) elementData[offset + (lastRet = i)];
    }

    那我们在next()方法后面,仿照CAS机制中的ABA问题,先添加元素然后删除元素,行不行呢?(这样做毫无意义)

    答案是不行的:

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
    
        try {
               SubList.this.remove(lastRet);
               cursor = lastRet;
               lastRet = -1;
               expectedModCount = ArrayList.this.modCount;
        } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
        }
    }

    因为remove中也调用了上面的那个方法。但是我们可以用一种方法,在遍历的过程中把需要删除的对象保存到一个集合中,等遍历结束后再调用removeAll()方法来删除,或者使用iterator.remove()方法。

    3.为什么我们需要ConcurrentHashMap和CopyOnWriteArrayList?

    同步的集合类(Hashtable和Vector),同步的封装类(使用Collections.synchronizedMap()方法和Collections.synchronizedList()方法返回的对象)可以创建出线程安全的Map和List。但是有些因素使得它们不适合高并发的系统。它们仅有单个锁,对整个集合加锁,以及为了防止ConcurrentModificationException异常经常要在迭代的时候要将集合锁定一段时间,这些特性对可扩展性来说都是障碍。

    4.为什么用CopyOnWriteArrayList代替ArrayList,就不会产生ConcurrentModificationException?

    从字面意思上可以看出来复制出来一份来操作,然后写道原先的ArrayList中。这样说肯定不行,我们看看源码,验证字面意思对不对?

    既然在迭代的时候能add,我们先看看add的实现:

    /**
    *1、获取互斥锁
    *2、Copy当前元素数组创建新数组,数组内存空间增1
    *3、添加元素
    *4、请求容器数据数组内存地址变更为新数组地址
    *5、释放互斥锁
    *6、返回结果
    **/
    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();
            }
        }

    那还有一个问题,在调用Iterator的next方法,结果会是什么样的呢?边add边迭代吗?

    /**
     * Cme表示ConcurrentModificationException
     * 目的是消除ConcurrentModificationException
     */
    public class Cme2 implements Runnable{
        List list = new CopyOnWriteArrayList();
    
        @Override
        public void run() {
            list.add(1);
            list.add(2);
    
            Iterator itr = list.listIterator();
            while (itr.hasNext()) {
                System.out.println(itr.next());
                list.add(3);
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Cme2 cm = new Cme2();
            Thread thread_1 = new Thread(cm);
            thread_1.start();
    
        }
    }

    运行结果:

    1
    2

    看看原因:

    public Iterator<E> iterator() {
            return new COWIterator<E>(getArray(), 0);
        }
    static final class COWIterator<E> implements ListIterator<E> {
            /** Snapshot of the array */
            private final Object[] snapshot;
            /** Index of element to be returned by subsequent call to next.  */
            private int cursor;
    
            private COWIterator(Object[] elements, int initialCursor) {
                cursor = initialCursor;
                snapshot = elements;
            }
    
            public boolean hasNext() {
                return cursor < snapshot.length;
            }
    
            public boolean hasPrevious() {
                return cursor > 0;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                if (! hasNext())
                    throw new NoSuchElementException();
                return (E) snapshot[cursor++];
            }
    }

    并发场景下对容器的添加操作是通过在容器内部数据数组的副本来完成的。对容器的迭代使用的是容器原始数据数组因为迭代不会产生修改,因此多个线程可以同时对容器进行迭代,而不会对彼此干扰或影响修改容器的线程。

    5.为什么用ConcurrentHashMap代替hashMap,就不会产生ConcurrentModificationException?

    /**
     * Cme表示ConcurrentModificationException
     * 目的是制造ConcurrentModificationException
     */
    public class Cme implements Runnable{
        Map list = new HashMap();
    
        @Override
        public void run() {
            list.put('1',1);
            list.put('2',2);
    
            Iterator itr = list.values().iterator();
            while (itr.hasNext()) {
                System.out.println(itr.next());
                list.put('3',3);
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Cme cm = new Cme();
            Thread thread_1 = new Thread(cm);
            thread_1.start();
    
        }
    }

    运行结果:

    1
    Exception in thread "Thread-0" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
        at java.util.HashMap$ValueIterator.next(HashMap.java:1466)
        at dp.Cme.run(Cme.java:19)
        at java.lang.Thread.run(Thread.java:745)

    看看原因:

    final Node<K,V> nextNode() {
                Node<K,V>[] t;
                Node<K,V> e = next;
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                if (e == null)
                    throw new NoSuchElementException();
                if ((next = (current = e).next) == null && (t = table) != null) {
                    do {} while (index < t.length && (next = t[index++]) == null);
                }
                return e;
    }

    还是期望值与传进来的元素数量不相等所导致的。

    解决办法:

    由于map是key-value形式的,并且map是有自己的空间的,所以在put的时候,并不影响。
    在迭代时是如何保证遍历出原来的map的value的,具体原因我会再深入,及时修改文章。

    6.hashtable怎么就不能代替在多线程情况下的hashMap?

    Hashtable和ConcurrentHashMap都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。也就是说代替是可以代替,但是在解决线程安全的同时降低了性能,所以选用ConcurrentHashMap。

    7.为什么要在多线程下使用hashMap呢?

    这是废话。线程不安全你还偏要用,非得跟自己过不去吗?

    参考资料:

    CocurrentHashMap和Hashtable的区别         

    Iterator

    JCIP_5_01_CopyOnWriteArrayList为什么不会产生ConcurrentModificationException

    老生常谈,HashMap的死循环

    理解和解决Java并发修改异常ConcurrentModificationException

    原创不易,转载请注明地址http://www.cnblogs.com/huhu1203/p/8445827.html ,如有问题,请指正!

  • 相关阅读:
    [System.currentTimeMillis]/[Calendar.getInstance().getTimeInMillis()]/[new Date().getTime()]
    [Maven]告警[WARNING] Unable to create Maven project from repository.
    通过设置JDK解决存在多个Gradle后台进程的问题
    使用OpenSSL实现X25519秘钥协商功能
    C++代码质量度量工具大阅兵
    Java代码质量度量工具大阅兵
    计算两个时间的时间差(天、小时、分钟、秒数)
    上传文件进度条 (已上传大小、总大小、速度、剩余时间、已用时间)
    table 表格自适应
    谷歌浏览器input框添加了黄色 背景色
  • 原文地址:https://www.cnblogs.com/huhu1203/p/8445827.html
Copyright © 2020-2023  润新知