>  기사  >  Java  >  Java 다중 스레드 액세스의 일반적인 예외 - 빠른 손실(빠른 실패)

Java 다중 스레드 액세스의 일반적인 예외 - 빠른 손실(빠른 실패)

黄舟
黄舟원래의
2017-02-28 10:58:301433검색


JDK 컬렉션에서는 다음과 유사한 단어를 자주 볼 수 있습니다.

예를 들어 ArrayList:

일반적으로 다음과 같은 이유로 반복자의 빠른 실패 동작을 보장할 수 없습니다. 동기화되지 않은 동시 수정이 발생할지 여부를 확실하게 보장할 수는 없습니다. 빠른 실패 반복자는 최선을 다해 ConcurrentModificationException을 발생시킵니다. 따라서 반복자의 정확성을 향상시키기 위해 이 예외에 의존하는 프로그램을 작성하는 것은 좋지 않습니다. 반복자의 빠른 실패 동작은 버그를 탐지하는 데에만 사용해야 합니다.

HashMap:

일반적으로 비동기 동시 수정이 있는 경우 반복자의 빠른 실패 동작을 보장할 수 없습니다. 빠른 실패 반복자는 최선을 다해 ConcurrentModificationException을 발생시킵니다. 따라서 이 예외에 의존하는 프로그램을 작성하는 것은 실수입니다. 올바른 접근 방식은 반복자의 빠른 실패 동작을 프로그램 오류를 감지하는 데에만 사용해야 한다는 것입니다.

이 두 문단에서는 '빠른 실패'가 반복해서 언급됩니다. 그렇다면 "빠른 실패" 메커니즘은 무엇입니까?

"빠른 실패"는 빠른 실패이며, 이는 Java 컬렉션의 오류 감지 메커니즘입니다. 여러 스레드가 컬렉션에 대한 구조적 변경을 수행하면 빠른 실패 메커니즘이 발생할 수 있습니다. 확실하지는 않지만 가능하다는 것을 기억하십시오. 예: 두 개의 스레드(스레드 1, 스레드 2)가 있다고 가정합니다. 스레드 1이 Iterator를 통해 세트 A의 요소를 순회하고 있다고 가정합니다. 어떤 시점에서 스레드 2는 세트 A의 구조를 수정합니다. 간단히 컬렉션 요소의 내용을 수정하면 프로그램은 이때
ConcurrentModificationException 예외를 발생시켜 빠른 실패 메커니즘을 생성합니다.


1. 빠른 실패 예

public class FailFastTest {  
    private static List<Integer> list = new ArrayList<>();  
    /** 
     * @desc:线程one迭代list 
     * @Project:test 
     * @file:FailFastTest.java 
     * @Authro:chenssy 
     * @data:2014年7月26日 
     */  
    private static class threadOne extends Thread{  
        public void run() {  
            Iterator<Integer> iterator = list.iterator();  
            while(iterator.hasNext()){  
                int i = iterator.next();  
                System.out.println("ThreadOne 遍历:" + i);  
                try {  
                    Thread.sleep(10);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
    /** 
     * @desc:当i == 3时,修改list 
     * @Project:test 
     * @file:FailFastTest.java 
     * @Authro:chenssy 
     * @data:2014年7月26日 
     */  
    private static class threadTwo extends Thread{  
        public void run(){  
            int i = 0 ;   
            while(i < 6){  
                System.out.println("ThreadTwo run:" + i);  
                if(i == 3){  
                    list.remove(i);  
                }  
                i++;  
            }  
        }  
    }  
    public static void main(String[] args) {  
        for(int i = 0 ; i < 10;i++){  
            list.add(i);  
        }  
        new threadOne().start();  
        new threadTwo().start();  
    }  
}



실행 결과:

ThreadOne 遍历:0  
ThreadTwo run:0  
ThreadTwo run:1  
ThreadTwo run:2  
ThreadTwo run:3  
ThreadTwo run:4  
ThreadTwo run:5  
Exception in thread "Thread-0" java.util.ConcurrentModificationException  
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)  
    at java.util.ArrayList$Itr.next(Unknown Source)  
    at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)


2. -fast의 이유

위의 예와 설명을 통해 처음에는 Fail-Fast의 이유가 프로그램이 컬렉션을 반복할 때 스레드가 컬렉션을 구조적으로 수정하고 반복자가 이를 수행하기 때문이라는 것을 처음에 알았습니다. ConcurrentModificationException 예외 정보가 발생하여 빠른 실패가 발생합니다.

빠른 실패 메커니즘을 이해하려면 먼저 ConcurrentModificationException 예외를 이해해야 합니다. 이 예외는 메소드가 객체의 동시 수정을 감지했지만 그러한 수정을 허용하지 않는 경우 발생합니다. 동시에, 이 예외는 객체가 다른 스레드에 의해 동시에 수정되었음을 항상 나타내지는 않는다는 점에 유의해야 합니다. 단일 스레드가 규칙을 위반하면 예외가 발생할 수도 있습니다.

반복자의 빠른 실패 동작을 보장할 수 없고 이 오류가 발생한다고 보장할 수 없는 것은 사실이지만 빠른 실패 작업은 ConcurrentModificationException을 발생시키기 위해 최선을 다할 것이므로 이러한 작업의 정확성을 향상시킵니다. 이 예외에 의존하는 프로그램을 작성하는 것은 실수입니다. 올바른 접근 방식은 다음과 같습니다. ConcurrentModificationException은 버그를 감지하는 데에만 사용해야 합니다. 아래에서는 ArrayList를 예로 사용하여 빠른 실패의 이유를 추가로 분석하겠습니다.

앞서 우리는 iterator를 작동할 때 Fail-Fast가 생성된다는 것을 알고 있습니다. 이제 ArrayList에 있는 반복자의 소스 코드를 살펴보겠습니다.


private class Itr implements Iterator<E> {  
        int cursor;  
        int lastRet = -1;  
        int expectedModCount = ArrayList.this.modCount;  
        public boolean hasNext() {  
            return (this.cursor != ArrayList.this.size);  
        }  
        public E next() {  
            checkForComodification();  
            /** 省略此处代码 */  
        }  
        public void remove() {  
            if (this.lastRet < 0)  
                throw new IllegalStateException();  
            checkForComodification();  
            /** 省略此处代码 */  
        }  
        final void checkForComodification() {  
            if (ArrayList.this.modCount == this.expectedModCount)  
                return;  
            throw new ConcurrentModificationException();  
        }  
    }

위의 소스 코드에서 반복자가 next(), delete( ) checkForComodification() 메서드는 항상 호출됩니다. 이 메서드는 주로 modCount == ExpectModCount?를 감지하는 데 사용됩니다. 그렇지 않은 경우 ConcurrentModificationException 예외가 발생하여 빠른 실패 메커니즘이 생성됩니다. 따라서 빠른 실패 메커니즘이 발생하는 이유를 파악하려면 modCount != ExpectModCount
이유와 해당 값이 언제 변경되었는지 파악해야 합니다.

ExpectModCount는 Itr에 정의되어 있습니다. int ExpectModCount = ArrayList.this.modCount; 따라서 해당 값은 수정할 수 없으므로 변경되는 것은 modCount입니다. modCount는 AbstractList에 정의되어 있으며 전역 변수입니다.


protected transient int modCount = 0;

그럼 언제, 어떤 이유로 변경되나요? ArrayList의 소스 코드를 참조하세요:


public boolean add(E paramE) {  
    ensureCapacityInternal(this.size + 1);  
    /** 省略此处代码 */  
}  
private void ensureCapacityInternal(int paramInt) {  
    if (this.elementData == EMPTY_ELEMENTDATA)  
        paramInt = Math.max(10, paramInt);  
    ensureExplicitCapacity(paramInt);  
}  
private void ensureExplicitCapacity(int paramInt) {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}  
ublic boolean remove(Object paramObject) {  
    int i;  
    if (paramObject == null)  
        for (i = 0; i < this.size; ++i) {  
            if (this.elementData[i] != null)  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    else  
        for (i = 0; i < this.size; ++i) {  
            if (!(paramObject.equals(this.elementData[i])))  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    return false;  
}  
private void fastRemove(int paramInt) {  
    this.modCount += 1;   //修改modCount  
    /** 省略此处代码 */  
}  
public void clear() {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}

 

       从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:

       有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount  = N  ,而modCount
= N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

       所以,直到这里我们已经完全了解了fail-fast产生的根本原因了。知道了原因就好找解决办法了。


三、fail-fast解决办法

通过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:

       方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

       方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

       CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?

       第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。

       第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:

private static class COWIterator<E> implements ListIterator<E> {  
        /** 省略此处代码 */  
        public E next() {  
            if (!(hasNext()))  
                throw new NoSuchElementException();  
            return this.snapshot[(this.cursor++)];  
        }  
        /** 省略此处代码 */  
    }

       CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:

public boolean add(E paramE) {  
        ReentrantLock localReentrantLock = this.lock;  
        localReentrantLock.lock();  
        try {  
            Object[] arrayOfObject1 = getArray();  
            int i = arrayOfObject1.length;  
            Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
            arrayOfObject2[i] = paramE;  
            setArray(arrayOfObject2);  
            int j = 1;  
            return j;  
        } finally {  
            localReentrantLock.unlock();  
        }  
    }  
    final void setArray(Object[] paramArrayOfObject) {  
        this.array = paramArrayOfObject;  
    }

 

       CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
arrayOfObject2[i] = paramE;  
setArray(arrayOfObject2);

       就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。

       所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。

 以上就是Java 的多线程访问常见异常--fast-lost (快速失败 ) 的内容,更多相关内容请关注PHP中文网(www.php.cn)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.