웹사이트 검색

java.util.ConcurrentModificationException


java.util.ConcurrentModificationException은 Java 컬렉션 클래스로 작업할 때 매우 일반적인 예외입니다. Java 컬렉션 클래스는 실패가 빠릅니다. 즉, 일부 스레드가 반복자를 사용하여 컬렉션을 순회하는 동안 컬렉션이 변경되면 iterator.next()가 ConcurrentModificationException을 발생시킵니다. 동시 수정 예외는 다중 스레드 및 단일 스레드 Java 프로그래밍 환경의 경우에 발생할 수 있습니다.

java.util.ConcurrentModificationException

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class ConcurrentModificationExceptionExample {

	public static void main(String args[]) {
		List<String> myList = new ArrayList<String>();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator<String> it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map<String, String> myMap = new HashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("2")) {
				myMap.put("1", "4");
				// myMap.put("4", "4");
			}
		}

	}
}

위의 프로그램은 아래 콘솔 로그에 표시된 것처럼 실행될 때 java.util.ConcurrentModificationException을 발생시킵니다.

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

출력 스택 추적에서 iterator next() 함수를 호출할 때 동시 수정 예외가 발생한다는 것이 분명합니다. Iterator가 수정 사항을 확인하는 방법이 궁금하다면 int 변수 modCount가 정의된 AbstractList 클래스에 Iterator 구현이 있습니다. modCount는 목록 크기가 변경된 횟수를 제공합니다. modCount 값은 모든 next() 호출에서 checkForComodification() 함수의 수정 사항을 확인하는 데 사용됩니다. 이제 목록 부분을 주석 처리하고 프로그램을 다시 실행하십시오. 이제 발생하는 ConcurrentModificationException이 없음을 알 수 있습니다. 산출:

Map Value:3
Map Value:2
Map Value:4

myMap에서 기존 키 값을 업데이트하고 있기 때문에 크기가 변경되지 않았으며 ConcurrentModificationException이 발생하지 않습니다. HashMap 키셋은 목록처럼 정렬되지 않기 때문에 시스템에서 출력이 다를 수 있습니다. HashMap에서 새 키-값을 추가하는 명령문의 주석을 제거하면 ConcurrentModificationException이 발생합니다.

다중 스레드 환경에서 ConcurrentModificationException을 방지하려면

  1. 목록을 배열로 변환한 다음 배열에서 반복할 수 있습니다. 이 접근 방식은 작거나 중간 크기의 목록에 적합하지만 목록이 크면 성능에 많은 영향을 미칩니다.
  2. 동기화 블록에 넣어 반복하는 동안 목록을 잠글 수 있습니다. 이 접근 방식은 멀티스레딩의 이점을 중단하므로 권장되지 않습니다.
  3. JDK1.5 이상을 사용하는 경우 ConcurrentHashMap 및 CopyOnWriteArrayList 클래스를 사용할 수 있습니다. 이것은 동시 수정 예외를 피하기 위해 권장되는 접근 방식입니다.

단일 스레드 환경에서 ConcurrentModificationException을 방지하려면

반복자 remove() 함수를 사용하여 기본 컬렉션 개체에서 개체를 제거할 수 있습니다. 그러나 이 경우 목록에서 다른 개체가 아닌 동일한 개체를 제거할 수 있습니다. Concurrent Collection 클래스를 사용하여 예제를 실행해 보겠습니다.

package com.journaldev.ConcurrentModificationException;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class AvoidConcurrentModificationException {

	public static void main(String[] args) {

		List<String> myList = new CopyOnWriteArrayList<String>();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator<String> it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3")) {
				myList.remove("4");
				myList.add("6");
				myList.add("7");
			}
		}
		System.out.println("List Size:" + myList.size());

		Map<String, String> myMap = new ConcurrentHashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("1")) {
				myMap.remove("3");
				myMap.put("4", "4");
				myMap.put("5", "5");
			}
		}

		System.out.println("Map Size:" + myMap.size());
	}

}

위 프로그램의 출력은 아래와 같습니다. 프로그램에서 발생하는 ConcurrentModificationException이 없음을 알 수 있습니다.

List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:2
Map Value:4
Map Value:5
Map Size:4

위의 예에서 다음이 분명합니다.

  1. Concurrent Collection classes can be modified safely, they will not throw ConcurrentModificationException.

  2. In case of CopyOnWriteArrayList, iterator doesn’t accommodate the changes in the list and works on the original list.

  3. In case of ConcurrentHashMap, the behaviour is not always the same.For condition:

    if(key.equals("1")){
    	myMap.remove("3");}
    

    Output is:

    Map Value:1
    Map Value:null
    Map Value:4
    Map Value:2
    Map Size:4
    

    It is taking the new object added with key “4” but not the next added object with key “5”. Now if I change the condition to below.

    if(key.equals("3")){
    	myMap.remove("2");}
    

    Output is:

    Map Value:1
    Map Value:3
    Map Value:null
    Map Size:4
    

    In this case, it’s not considering the newly added objects. So if you are using ConcurrentHashMap then avoid adding new objects as it can be processed depending on the keyset. Note that the same program can print different values in your system because HashMap keyset is not ordered.

for 루프를 사용하여 java.util.ConcurrentModificationException 방지

단일 스레드 환경에서 작업 중이고 코드에서 목록에 추가된 객체를 처리하도록 하려면 Iterator 대신 for 루프를 사용하면 됩니다.

for(int i = 0; i<myList.size(); i++){
	System.out.println(myList.get(i));
	if(myList.get(i).equals("3")){
		myList.remove(i);
		i--;
		myList.add("6");
	}
}

동일한 개체를 제거하기 때문에 카운터를 줄이고 있다는 점에 유의하십시오. 다음 또는 더 멀리 있는 개체를 제거해야 하는 경우 카운터를 줄일 필요가 없습니다. 직접 해보십시오. :) 한 가지 더: subList로 원래 목록의 구조를 수정하려고 하면 ConcurrentModificationException이 발생합니다. 간단한 예를 들어 보겠습니다.

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

		List<String> names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

		List<String> first2Names = names.subList(0, 2);

		System.out.println(names + " , " + first2Names);

		names.set(1, "JavaScript");
		// check the output below. :)
		System.out.println(names + " , " + first2Names);

		// Let's modify the list size and get ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

위 프로그램의 출력은 다음과 같습니다.

[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
	at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
	at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
	at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
	at java.base/java.lang.String.valueOf(String.java:2801)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)

ArrayList subList 문서에 따르면 구조 수정은 subList 메서드에서 반환된 목록에서만 허용됩니다. 반환된 목록의 모든 메서드는 먼저 지원 목록의 실제 modCount가 예상 값과 같은지 확인하고 그렇지 않은 경우 ConcurrentModificationException을 발생시킵니다.

GitHub 리포지토리에서 모든 예제 코드를 다운로드할 수 있습니다.