美文网首页
关于安全失败机制与快速失败机制

关于安全失败机制与快速失败机制

作者: 明月清风被占用 | 来源:发表于2020-03-30 11:39 被阅读0次
    什么是安全失败与快速失败?
    • 在java.util包下的集合类,它们都有这样一个安全特性,在并发情况下用iterator遍历集合中的元素的时候,如果有线程A在遍历这个集合的同时,同时线程B执行了操作改变了集合的数据结构(增删改),那么程序会直接中断抛出 Concurrent Modification Exception,这种机制,就称作快速失败机制。

    • 安全失败机制是集合在被遍历的时候,这个遍历的对象是这个集合的一个副本,也就是操作的不是这个集合对象本身,那么每个线程操作的都是自己获得的一份拷贝,其他线程对这个拷贝的改变自然不会影响到自身遍历的集合。
    快速失败原理
    • 快速失败:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext() / next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
    • 论据
          //这是abstractlist下定义的modCount
          protected transient int modCount = 0;
            //ArrayList中的Iterator源码
            int expectedModCount = modCount;
    
    ---------------------华丽分割线--------------------------------
    
    //来看一个测试用例
            List<String> jpanMeiZi = new ArrayList<>();  //一个日本妹子集合
            jpanMeiZi.add("有村架纯");
            jpanMeiZi.add("新垣结衣");
            jpanMeiZi.add("石原里美");
            jpanMeiZi.add("斋藤飞鸟");
            Iterator<String> iterator = jpanMeiZi.iterator();   //获取迭代器
            while(iterator.hasNext()) {
                String str = iterator.next();
                if(str.equals("新垣结衣")) {   //由于Gakki有男朋友了,所以当我们遇到她的时候,是时候对他放手了。把她拿掉
                    jpanMeiZi.remove("新垣结衣");
                }
                System.out.println(str);
            }
    
            //控制台结果
            有村架纯
            新垣结衣
            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.ssp.share.test.Test02.main(Test02.java:17)
            Process finished with exit code 1   
    

    现在我们从源码角度来看看它为什么会抛出异常

    按照流程走:
    ----1、添加元素:
    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // 判断数组容量是否支撑再添加一个元素
            elementData[size++] = e;
            return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
            //在abstractlist中定义的变量  protected transient int modCount = 0;
           //这个变量代表了集合实际被修改的次数,很重要
            modCount++;
    
            //下面就是扩容过程,不解释
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
    }
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    那么,从这样一个添加元素的过程,我们可以观察到,这个modelCount代表的是这个集合实际上被修改的次数。而我们给出的这段代码中,添加了4个元素,每次添加一个元素,它都会进行一个 modelCount++ 操作,所以它在进入这个 while 循环之前,它的值变成了4。

            jpanMeiZi.add("有村架纯");
            jpanMeiZi.add("新垣结衣");
            jpanMeiZi.add("石原里美");
            jpanMeiZi.add("斋藤飞鸟");
            Iterator<String> iterator = jpanMeiZi.iterator();   //获取迭代器
            while(iterator.hasNext()) {
                String str = iterator.next();
                if(str.equals("新垣结衣")) {  候,是时候对他放手了。把她拿掉
                    jpanMeiZi.remove("新垣结衣");
                }
                System.out.println(str);
            }
    

    现在就是重头戏了

    进入了 while 循环,执行了里面的语句:
       第一条:
            public boolean hasNext() {
                //返回为true,继续执行循环体内容,size指代集合实际元素个数
                //这个cursor它指代一个指向当前元素的一个游标,默认为0
                return cursor != size;   
            }
       第二条:
            while(iterator.hasNext()) {
                String str = iterator.next();   <------执行这条,看编号 1>
                if(str.equals("新垣结衣")) {   
                    jpanMeiZi.remove("新垣结衣");
                }
                System.out.println(str);
            }
             // 1>我们进入这个方法看看它都做了些什么
             public E next() {
                  //每次读取元素的时候,它都要检查一个这个modelCount的数值有没有被修改。详细看编号2>
                  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];
              }
              // 2>
              final void checkForComodification() {
                  //这里就是比较的地方,这个expectedModCount是预期集合被修改的次数
                  //它被定义在Itr这个类下,int expectedModCount = modCount; 具体看编号3>
                  //集合如果被修改,那么这个modCount就会被++操作,自然不等于这个expectedModCount
                  if (modCount != expectedModCount)
                      throw new ConcurrentModificationException();
              }
    // 3> 这个类实现了Iterator接口,自然也就实现了它的方法,而我们所使用的迭代器,就是
    //这个类的实例,调用的方法自然就是这个类下的方法了
    
        public Iterator<E> iterator() {
            return new Itr();  // <--------------创建了一个实例
        }
        private class Itr implements Iterator<E> {
            int cursor;      
            int lastRet = -1; 
            int expectedModCount = modCount;   //<---这里,modelCount是在ArrayList父类中定义的一个变量0
    
            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();
                }
            }
    
            @Override
            @SuppressWarnings("unchecked")
            public void forEachRemaining(Consumer<? super E> consumer) {
                Objects.requireNonNull(consumer);
                final int size = ArrayList.this.size;
                int i = cursor;
                if (i >= size) {
                    return;
                }
                final Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length) {
                    throw new ConcurrentModificationException();
                }
                while (i != size && modCount == expectedModCount) {
                    consumer.accept((E) elementData[i++]);
                }
                // update once at end of iteration to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
    
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }
    
        第三条:
            public E next() {
                checkForComodification();   
                int i = cursor;   //<---------这里,cursor开始为0,下面的操作就是移动这个游标和进行一个判断操作
                if (i >= size)
                    throw new NoSuchElementException();
                Object[] elementData = ArrayList.this.elementData;  //这个ArrayList.this是指我们创建的list对象
                if (i >= elementData.length)
                    throw new ConcurrentModificationException();
                cursor = i + 1;  //指向下一个
                return (E) elementData[lastRet = i];  //返回当前遍历出的元素
            }
        第四条:
            while(iterator.hasNext()) {
                String str = iterator.next();
                if(str.equals("新垣结衣")) {     //<----执行这里,看看里面做了些什么
                    jpanMeiZi.remove("新垣结衣");
                }
                System.out.println(str);
            }
            //做的一些判断
            public boolean equals(Object anObject) {
              if (this == anObject) {
                  return true;
              }
              if (anObject instanceof String) {
                  String anotherString = (String)anObject;
                  int n = value.length;
                  if (n == anotherString.value.length) {
                      char v1[] = value;
                      char v2[] = anotherString.value;
                      int i = 0;
                      while (n-- != 0) {
                          if (v1[i] != v2[i])
                              return false;
                          i++;
                      }
                      return true;
                  }
            }
            return false;
        }
    
        第五条:
                String str = iterator.next();
                if(str.equals("新垣结衣")) { 
                    jpanMeiZi.remove("新垣结衣");    //<---------------执行这里
                }
                
                public boolean remove(Object o) {
                    if (o == null) {
                        for (int index = 0; index < size; index++)
                            if (elementData[index] == null) {
                                fastRemove(index);
                            return true;
                        }
                    } else {         //<-----------------执行这里
                        for (int index = 0; index < size; index++)
                            if (o.equals(elementData[index])) {
                                fastRemove(index);          //<-----------这才是真正执行删除的方法
                            return true;
                        }
                    }
                    return false;
                 }
              
                private void fastRemove(int index) {
                    modCount++;   //  如果执行了删除进行++操作,后继便于抛出异常
                    int numMoved = size - index - 1;
                    if (numMoved > 0)  
                        System.arraycopy(elementData, index+1, elementData, index,numMoved);
                    elementData[--size] = null;   //将后置的元素制空,让GC回收
                }
    
       第六条:
              while(iterator.hasNext()) {
                  String str = iterator.next();
                  if(str.equals("新垣结衣")) {   
                      jpanMeiZi.remove("新垣结衣");
                  }
                  System.out.println(str);   //<-----------执行打印
              }
        第七条:
              while(iterator.hasNext()) {   //<-----------执行此处,后继就和前面差不多了
                  String str = iterator.next();   //<----------此处执行结束,因为结构上发生了改
                                                   // 变,即modCount != expectedModCount mod为5,ex为4,抛出异常,终止程序
                  if(str.equals("新垣结衣")) {   
                      jpanMeiZi.remove("新垣结衣");
                  }
                  System.out.println(str);
               }
    
    

    至此,对快速失败的总结。一个线程在通过迭代器遍历这个集合的同时,另外一个线程在对这个集合进行结构上的改变。由于操纵的是这个集合本身,导致异常抛出。

    • 额外补充:
      \color{#FF3030}{ 不多,值得注意的一个点是,在快速失败机制的支持下,有一种结构方式的改变不会引起异常的抛出,下面我们来看看: }
      ----->  还是这个例子,不过注意,此时的集合被我删除了一个元素,现在只有三个
    
            List<String> jpanMeiZi = new ArrayList<>();  //一个日本妹子集合
            jpanMeiZi.add("有村架纯");
            jpanMeiZi.add("新垣结衣");
            jpanMeiZi.add("石原里美");         // <-----这里打断点,开始Debug
            Iterator<String> iterator = jpanMeiZi.iterator();   //获取迭代器
            while(iterator.hasNext()) {
                String str = iterator.next();
                if(str.equals("新垣结衣")) { 
                    jpanMeiZi.remove("新垣结衣");
                }
                System.out.println(str);
            }
    

    那么我们来看看,它会发生什么?下面方法按顺序执行:

    public boolean hasNext() {
            return cursor != size;          //<-------这个光标需要注意,开始的时候它是0 != 3 返回true
    }
    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;    //<--------光标变成了1
                return (E) elementData[lastRet = i];
            }
    
    final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    
    //执行完第一个
    控制台输出:有村架纯
    
    //继续执行
    public boolean hasNext() {
                return cursor != size;     //<------- 1 != 3 返回true 
    }
    
    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;      //<-------  cursor 变成了2
                return (E) elementData[lastRet = i];
    }
    
    final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
    }
    
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    
    while(iterator.hasNext()) {
                String str = iterator.next();
                if(str.equals("新垣结衣")) {     // <----------因为第二元素是新垣结衣,所以这里会执行删除
                    jpanMeiZi.remove("新垣结衣");      // <-----------执行remove方法
                }
                System.out.println(str);
    }
    
    public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);       //<-----------真正执行删除的方法
                        return true;
                    }
            }
            return false;
    }
    
    private void fastRemove(int index) {
            modCount++;    //<------------因为是结构上的改变,所以modCount++,此时为4
            int numMoved = size - index - 1;  
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // <----- 这里需要关注这个size,删除一个元素之后,它变成了2
    }
    
    public boolean hasNext() {
                return cursor != size;      //<---------这个curore=2  size=2 ,
                                          //由于删除一个元素之后,这个方法返回了false
                                          //所以,后继它不会再去执行并发异常检测
            }
    
                                            //程序结束,打印移除元素之外的集合内容
    

    对于这种情况,当且仅当你要删除的元素在倒数第二个的时候,也就是说你的cursor = size 的时候,并发异常不会抛出。这个是需要注意的地方,具体可以实操更多的例子去作证。

    安全失败原理
    • 通过迭代器迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
    • CopyOnWriteArrayList 集合 是遍历时能安全删除元素的代表集合。
      在了解这个集合之前,我们有必要先知道这个集合的单词核心意思,CopyOnWrite,它是指写入时复制。

    写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

    到这里,我们大致可以了解到这个集合的创建意图。当A线程在读取这个集合时,B线程在操作这个集合的结构,由于俩者操纵的不是同一个集合对象,B线程得到的是指向这个集合的一个副本,所以它的修改并不会影响到其他线程去读取原集合本身。

    --------------华丽分割线--------------------------------------------

    那么我们现在从源码角度来看看,写操作的一个执行的流程:

            public boolean add(E e) {
                final ReentrantLock lock = this.lock;     //可重入锁,写操作需要加锁
                lock.lock();   //从这里可以看出,这个线程在添加元素的时候是需要上锁的,目的避免了同时复制了N个副本
                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();  //释放锁
                }
            }
            final Object[] getArray() {
                return array;
            }
          
    

    读操作的流程:下面方法依次有递进关系,可以很明显的看到,读是在同一个源数组上,而不是副本,且读操作没有加上锁,这相对于与采取加锁的方式,性能提升不少

    public E get(int index) {
            return get(getArray(), index);
    }
        private E get(Object[] a, int index) {
            return (E) a[index];
        }
            final Object[] getArray() {
                return array;
            }
              private transient volatile Object[] array;
    

    脑容量有限,本文就到这里,下一节,我们继续看看这个可重入锁底层究竟在这个集合上干了些什么。

    相关文章

      网友评论

          本文标题:关于安全失败机制与快速失败机制

          本文链接:https://www.haomeiwen.com/subject/ymkjuhtx.html