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

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

作者: 明月清风被占用 | 来源:发表于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