ArrayList、LinkedHashMap等线程非安全的处理

之前在做剔除异常数的过程中,遇到过一次ConcurrentModificationException异常,当时的场景是这样的:利用迭代器去除map中的value然后与一个常数对比,比它小就删除。当时就抛出了这个异常,后来仔细研究才明白了这个问题的来龙去脉。

首先先看ArrayList 的文档:

Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList, its capacity grows automatically. The details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost.

An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be "wrapped" using the Collections.synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:

   List list = Collections.synchronizedList(new ArrayList(...));

The iterators returned by this class's iterator and listIterator methods are fail-fast:

if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or addmethods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs。

也就是说,在迭代器查询期间,如果对原始list发生数据结构的改变,增加或是删除,就会抛出 ConcurrentModificationException异常,但是如果我们需要对其进行比如删除操作该怎么办呢,上面也提供了一种解决方案,就是利用迭代器Iterator()的remove方法。之前用迭代器的时候,只是用next()和hasNext()两个方法,其实remove()用处也很大。可以看下面这个例子,其实ArrayList、LinkedHashMap遇到这种情况的解决方案都是类似的,下面例子用的LinkedHashMap.

Map map = new LinkedHashMap();
        map.put("ben",21);
        map.put("hep",18);
        map.put("audrey",15);
        Iterator> it = map.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry en = it.next();
            if(en.getValue()==15){
                it.remove();
            }else{
                System.out.println(en.getKey()+"---"+en.getValue());
            }
        }
        System.err.println(map.size());

最终输出的结果,map的size()等于2,所以也证明了确实实现了在迭代过程中的删除操作。

在调研的过程中还发现这么一个有趣的事情:

   fail-fast和fail-safe的区别

这里贴一段牛客网上非常详细的解释:

一:快速失败(fail—fast)

          在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改,则会抛出Concurrent Modification Exception。

          原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

      注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

      场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

 

之前看ArrayList源码的时候,有一个参数modCount不明白有什么用处,现在看了这个才明白这个参数的意义:

protected transient intmodCount = 0;

 

    二:安全失败(fail—safe)

      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

      原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

      缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

      场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

ok ,开始进行验证:

首先就是使用ConcurrentHashMap,然后在迭代器中进行原始map删除数据,看看是否会抛异常:

        Map map = new ConcurrentHashMap<>();
        map.put("ben",21);
        map.put("hep",18);
        map.put("audrey",15);
        Iterator> it = map.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry en = it.next();
            map.remove("ben");
        }
        System.err.println(map.size());

输出为2,所以可以看出对于ConcurrentHashMap这种线程安全的数据结构,是对原集合进行拷贝然后进行遍历的。

接下来,我们看看如果在迭代期间,原集合发生变化,迭代器是否知道,为了更加明显,我们对原数据进行修改操作:

Map map = new ConcurrentHashMap<>();
        map.put("ben",21);
        map.put("hep",18);
        map.put("audrey",15);
        Iterator> it = map.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry en = it.next();
            System.out.println("修改前:"+en.getKey()+":"+en.getValue());
            map.put("hep",88);
            System.out.println("修改后:"+en.getKey()+":"+en.getValue());
        }
        System.err.println(map.size());

其输出为:

ArrayList、LinkedHashMap等线程非安全的处理_第1张图片

与LinkedHashMap进行对比:

 Map map = new LinkedHashMap();
//        Map map = new ConcurrentHashMap<>();
        map.put("ben",21);
        map.put("hep",18);
        map.put("audrey",15);
        Iterator> it = map.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry en = it.next();
            System.out.println("修改前:"+en.getKey()+":"+en.getValue());
            map.put("hep",88);
            System.out.println("修改后:"+en.getKey()+":"+en.getValue());
        }

 

其输出为:

ArrayList、LinkedHashMap等线程非安全的处理_第2张图片

从而可以验证fail-safe模式确实是copy原集合,并且迭代期间,是不知道原集合的变化的;而fail-fast模式是直接在原集合中进行遍历,是可以知道原集合的变化的。

 

如果观察的仔细的话,会发现,我们在LinkedHashMap的迭代器中虽然不能实现删除操作,但是可以实现修改的操作,这是怎么回事呢?我们可以看一下fail-fast的官方定义:

* 

* The iterators returned by this class's {@link #iterator() iterator} and * {@link #listIterator(int) listIterator} methods are fail-fast: * if the list is structurally modified at any time after the iterator is * created, in any way except through the iterator's own * {@link ListIterator#remove() remove} or * {@link ListIterator#add(Object) add} methods, the iterator will throw a * {@link ConcurrentModificationException}. Thus, in the face of * concurrent modification, the iterator fails quickly and cleanly, rather * than risking arbitrary, non-deterministic behavior at an undetermined * time in the future.

文档清楚的指出 if the list is structurally modified at any time after the iterator is created 所以如果只是在迭代器中修改原集合,而不对原集合结构发生改变的化,是不会抛出ConcurrentModificationException异常的。突然想起泊松亮斑的实验。

你可能感兴趣的:(数据结构)