• Fail-Fast分析


    起因

    在分析并发问题的时候,想写一个错误的示例,就在for遍历的时候,调用list.remove(),此时应该抛出ConcurrentModificationException异常,但是奇怪的是它并没有抛出,我表示惊呆了。。。当时的代码如下:

    @Test
    public void test_for_remove(){
        List<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
    
        for(String temp : list){
            if("aa".equals(temp)){
                list.remove(temp);
            }
        }
       /* for(int i=0;i<list.size();i++){
            System.out.println(list.size());
            list.remove(i);
        }*/
        list.forEach(System.out::println);
    }
    

    深入理解原因

    其实,这也因为对报错的原因不熟悉导致的,在此记录一下报错的原因。foreach的语法糖其实还是Iterator的方式。

    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;
    
        public boolean hasNext() {
            return cursor != size;
        }
    
        @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];
        }
    
        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();
            }
        }
    	//...
    }
    

    上面有一个expectedModCount这个变量,实际上,ArrayList的父类AbstractList的实例中有一个变量modCount,用来记录对集合修改的次数,在每次集合变化后都会执行modCount++(这很关键)

    Iterator遍历逻辑是先去调用hasNext去判断是否存在有下一个元素,如果存在调用next方法去取。

    那么在那个地方报的ConcurrentModificationException呢?注意在next方法的第一行,checkForComodification()

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

    在Iterator实例化的时候,保存了modCount的副本expectedModCount,而后在迭代中调用list.remove,导致modCount发送了改变,在取next元素的时候,checkForModification方法内检测到modCount和expectedModCount不一致,throw new ConcurrentModificationException.

    为什么没有报错

    其实,之前知道list.remove会报错,但是对原理不是很清楚,现在搞懂了原理,在来看原来的代码。

      for(String temp : list){
            if("aa".equals(temp)){
                list.remove(temp);
            }
      }
    

    在list进行遍历的时候,list此时的size为2,调用remove后,size的值变成了1,而在hasNext的进行检查的时候,由于cursor == size,判断出已经结束了遍历,因此不会执行hasNext()方法,也不会报错了。。。。

    在删除倒数第二个元素的时候,单线程情况下不会报错。。。。

    解决方法的原理

    单线程下

    一般都会推荐使用iter.remove()的方式,来看一下为什么Iterator的remove方法不会导致报错

        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();
            }
        }
    

    在remove的代码中,更新了expectedModCount这个变量,因此在下次迭代的时候不会出现bug

    这种方式是有缺陷的:

    • 仅支持remove操作,不支持add和clear等操作
    • 仅适合单线程环境

    多线程环境下

    理论分析一下,假设有A、B两个线程,A线程在迭代的时候使用了iterator的remove方法,B线程仅仅是遍历,不做何修改操作。当A线程remove结束后,list的modCount发生了变化,A线程的exceptModCount随之变化,但是对于B线程,exceptModCount还是原来的值,在checkForComodification()的时候会报错。代码示例如下:

    @Test
    public void test_for_remove() throws InterruptedException {
        List<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        new Thread(()->{
            for(Iterator<String> iter = list.iterator();iter.hasNext();){
                if("cc".equals(iter.next())){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    iter.remove();
                }
            }
            System.out.println("end");
        }).start();
        new Thread(()->{
            for (String s:list){
                System.out.println(s);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Thread.sleep(5000L);
    }
    

    fail-fast机制

    参考文章: http://cmsblogs.com/?p=1220

    注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

    fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。

    回过头来看,一开始遇到的问题就是fail-fast机制没有起作用的情况,这样就很明确了。

    多线程的安全操作

    一般情况下,推荐使用CopyOnWriteArrayList。

    Copy-On-Write简称COW,,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    看一下CopyOnWriteArrayList是如何实现并发操作的。CopyOnWriteArrayList是实现了List接口,而非继承了AbstractList接口,就是说,要自己重新写Iterator接口,查看源码可以发现,CopyOnWriteArrayList的Iterator实现中放弃了fail-fast机制

    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++];
        }
    	public void remove() {
            throw new UnsupportedOperationException();
        }
    	...
    }
    

    在Iterator实现过程中并没有特殊的操作,需要注意的是,Iterator操作不支持remove方法,会直接抛出UnsupportedOperationException。

    实际上,CopyOnWriteArrayList的实现是靠加锁的方式来保证的。

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();
     /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#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();
        }
    }
    

    在每一次的add、remove等操作的时候,由锁来保证写一致。而且写的方式是:先复制出来一个新的容器,然后在新的容器中添加数据,添加完成以后,再将原来的数组引用指向新的容器。而读操作没有进行加锁,但是存在一个问题,如果在读的时候,有其他线程向List中加入了数据,那么读到的数据仍然是旧的数据,这样实现读写分离。

    实际上在上面的分析中,已经可以看出CopyOnWriteArrayList(可以扩展到其他的COW实现)中,至少存在以下的缺陷:

    • 内存开销大,每次写操作都会重新开辟一块内存区域,且每次写都要复制也造成了性能下降。从这个地方也看出,CopyOnWrite并发容器适用于读多写少的场景。
    • 数据一致性。CopyOnWrite只能保证数据是最终一致的,不能保证实时的一致性。因此如果要求实时的一致性,不能使用CopyOnWrite

    The last

    那么如何保证List的强一致性(实时一致),简单写一下,方式是利用ReentrantReadWriteLock来做:

    public class ReadWriteArrayList<E> implements List<E> {
        final transient ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 
        private Lock writeLock = rwl.writeLock();
     	private Lock readLock = rwl.readLock();
       
    	@Override
        public boolean add(E e) {
            writeLock.lock();
            try{
                //...
            }finally {
                writeLock.unlock();
            }
            return true;
        }
    @Override
    public Iterator<E> iterator() {
       return new COWIterator<E>(readLock);
    }
    static final class COWIterator<E> implements ListIterator<E>{
        private Lock lock;
        public COWIterator(Lock readLock) {
            this.lock= readLock;
        }
      @Override
        public E next() {
            lock.lock();
            try{
               //...
            }finally{
                lock.unlock();
            }
            return null;
        }
    }
  • 相关阅读:
    云小课|MRS基础原理之Oozie任务调度
    Java:既然有了synchronized,为什么还要提供Lock?
    云小课|MRS基础原理之CarbonData入门
    Workflow,要不要了解一下
    软件要想做的好,测试必定少不了
    什么是ForkJoin?看这一篇就能掌握!
    手把手教你从安装CentOS7.4镜像开始,搭建IoT视频监控系统
    案例解读华为隐私计算产品TICS如何实现城市跨部门数据隐私计算
    13个QA带你了解线上压测的知识点
    云小课|MRS基础原理之Hudi介绍
  • 原文地址:https://www.cnblogs.com/kakaxisir/p/5967727.html
Copyright © 2020-2023  润新知