java.util.ConcurrentModificationException

java.util.ConcurrentModificationException은 Java 컬렉션 클래스와 함께 작업할 때 매우 흔한 예외입니다. Java 컬렉션 클래스는 fail-fast하며, 즉, 컬렉션이 반복자(iterator)를 사용하여 트래버스할 동안 컬렉션이 변경되면 iterator.next()ConcurrentModificationException을 throw합니다. 동시 수정 예외는 다중 스레드 및 단일 스레드 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 myList = new ArrayList();

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

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

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

		Iterator 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을 throw합니다. 아래 콘솔 로그에서 확인할 수 있습니다.

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)

출력 스택 추적에서, 반복자 next() 함수를 호출할 때 동시 수정 예외가 발생하는 것이 명확합니다. 수정이 어떻게 확인되는지 궁금하다면, 이는 AbstractList 클래스에 구현되어 있는데, 여기서 int 변수 modCount가 정의됩니다. modCount는 리스트 크기가 변경된 횟수를 제공합니다. modCount 값은 checkForComodification() 함수에서 모든 next() 호출에 사용되어 변경 사항을 확인합니다. 이제 리스트 부분의 주석 처리를 해제하고 프로그램을 다시 실행하십시오. 이제 ConcurrentModificationException이 발생하지 않음을 확인할 것입니다. 출력:

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

기존 myMap의 키 값을 업데이트하고 있기 때문에 크기가 변경되지 않았으며 ConcurrentModificationException이 발생하지 않습니다. 출력은 시스템에 따라 다를 수 있습니다. 왜냐하면 HashMap 키 집합은 목록과 같이 정렬되어 있지 않기 때문입니다. HashMap에 새 키-값을 추가하는 문이 주석 처리되어 있으면 ConcurrentModificationException이 발생합니다.

다중 스레드 환경에서 ConcurrentModificationException을 피하는 방법

  1. 리스트를 배열로 변환한 다음 배열에서 반복할 수 있습니다. 이 접근 방식은 작거나 중간 크기의 리스트에 대해서는 잘 작동하지만, 리스트가 크면 성능에 영향을 미칠 수 있습니다.
  2. 리스트를 반복하는 동안 동기화된 블록에 넣어서 목록을 잠글 수 있습니다. 이 접근 방식은 멀티스레딩의 이점을 없앨 수 있기 때문에 권장되지 않습니다.
  3. JDK1.5 이상을 사용하는 경우 ConcurrentHashMapCopyOnWriteArrayList 클래스를 사용할 수 있습니다. 이것은 동시 수정 예외를 피하기 위한 권장되는 방법입니다.

단일 스레드 환경에서 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 클래스는 안전하게 수정할 수 있으며 ConcurrentModificationException을 발생시키지 않습니다.

  2. CopyOnWriteArrayList의 경우 반복자는 목록의 변경 사항을 수용하지 않고 원래 목록에서 작동합니다.

  3. ConcurrentHashMap의 경우, 동작은 항상 동일하지 않습니다. 다음 조건의 경우:

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

    출력 결과는 다음과 같습니다:

    맵 값: 1
    맵 값: null
    맵 값: 4
    맵 값: 2
    맵 크기: 4
    

    이는 “4” 키로 추가된 새 객체를 사용하지만 “5” 키로 추가된 다음 객체는 사용하지 않습니다. 이제 조건을 다음과 같이 변경합니다.

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

    출력 결과는 다음과 같습니다:

    맵 값: 1
    맵 값: 3
    맵 값: null
    맵 크기: 4
    

    이 경우에는 새로 추가된 객체를 고려하지 않습니다. 따라서 ConcurrentHashMap을 사용하는 경우 키셋에 따라 처리될 수 있는 새로운 객체 추가를 피하십시오. 동일한 프로그램이 시스템에 따라 다른 값을 출력할 수 있음을 유의하십시오. 왜냐하면 HashMap의 키셋은 정렬되지 않기 때문입니다.

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 names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

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

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

		names.set(1, "JavaScript");
		// 아래의 출력 확인해보세요. :)
		System.out.println(names + " , " + first2Names);

		// 목록 크기를 수정하고 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 문서에 따르면 structural modifications은 subList 메소드로 반환된 목록에서만 허용됩니다. 반환된 목록의 모든 메소드는 먼저 지원 목록의 실제 modCount가 예상 값과 동일한지 확인하고, 그렇지 않으면 ConcurrentModificationException을 throw합니다.

당신은 모든 예제 코드를 우리의 GitHub 저장소에서 다운로드할 수 있습니다.

Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception