
[Java] ConcurrentModificationException 해결하기
zl존석동
·2022. 2. 4. 19:18
ConcurrentModificationException이 왜 발생하나 간단하게 알아보고 해결해보자
언제 발생할까?
- MultiThread 또는 객체의 변경이 허용되지 않는 환경에서 '변화' 가 일어날 때 발생
- 한 쪽에서 Collection을 Iterating 할 때 다른 스레드에서 해당 Collection 변경을 할 경우 발생
- MultiThread 환경 뿐 아니라 fail-fast iterating 도중 변경이 일어나면 발생함
Fail-Fast Iteration
Fail Fast 라는 말 그대로 오류가 발생하면 즉시 던지고 작업을 중단하여 알려주는 방식이다.
Iterator 의 remove 메소드 이외의 코드로 Collection 수정 시 예외가 발생한다.
예외발생 예시
컬렉션중 ArrayList 를 예시로 하여 확인해보자
위에서 Iterator 말고 Collection 으로 직접 수정 시 예외가 발생한다고 했는데 다음과 같은 반복에서는 어떤 일이 일어날까?
List<String> list = new ArrayList<>();
list.add("김갑환");
list.add("최번개");
list.add("김거한");
list.add("김번개");
list.add("김갑환");
list.add("장거한");
list.add("장갑환");
list.add("김갑환");
list.add("김김김");
for (int i = 0; i < list.size(); i++) {
String str = list.get(i);
if (str.startsWith("김", 0)) {
list.remove(str);
}
}
위의 반복에서는 아무일도 일어나지 않는다.
배열에 저장된 값을 인덱스를 통해 하나씩 불러오고 로직에 의해 삭제되어도
For Loop에 의해 컬렉션의 사이즈가 계속해서 변경되기 때문에 전혀 문제가 없다.
하지만 아래와 같은 ForEach Loop 를 확인해보자
List<String> list = new ArrayList<>();
list.add("김갑환");
list.add("최번개");
list.add("김거한");
list.add("김번개");
list.add("김갑환");
list.add("장거한");
list.add("장갑환");
list.add("김갑환");
list.add("김김김");
for (String str : list) {
if (str.startsWith("김", 0)) {
list.remove(str);
}
}
아래와 같이 예외가 발생할 것이다.
왜 발생했을까?
예외 메시지를 보면
Foreach를 돌리는 도중 ArrayList 의 Itr 라는 클래스의 next 메소드에서 호출된 checkForComodification 이라는 메소드에서 예외가 발생하였다.
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
checkForComodification 메소드를 살펴보면 modCount 와 expectedModCount라는 변수를 비교하고 있고 다를 때
예외를 발생하고 있다.
Foreach 를 통해 ArrayList를 반복할 경우 맨 처음에 ArrayList 내부의 Iterator의 구현체인 Itr Class가 초기화 되는데
이 때 expectedModCount가 우리가 만든 ArrayList의 사이즈로 할당이 될 것이다.
그리고 이 값은 해당 클래스의 next() 메소드를 통해 ArrayList를 순회(Foreach)하는 동안 계속해서 modCount라는 것의 값과 비교를 당하게 된다.
이제 next() 메소드를 통해 반복의 한번을 시작하게 되어 ArrayList 의 remove(Object o) 메소드를 수행하게 될 것이다.
ArrayList의 remove(Object o) 메소드를 확인해보면 결국 다음과 같은 fastRemove(int index) 라는 메소드를 수행하게 된다,
Line 547을 보면 삭제 동작인데도 modCount를 하나 증가해주고 있다.
modCount는 컬렉션의 변화횟수에 대한 수치이기 때문에 그렇다.
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
이 modCount가 컬렉션 내부의 것 하나가 삭제될 때 증가함에도
반복을 위해 맨 처음에 초기화 했던 Itr 클래스의 expectedModCount 에 대한 수치는 계속 그대로이기 때문에 예외가 발생하는 것이다.
해결 방법
1. 직접 Iterator 사용
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
if (str.startsWith("김", 0)) {
iterator.remove();
}
}
2. removeIf 메소드 사용
removeIf(Predicate<? super E> filter)
- Java 8에서 나왔다고 한다.
- 1번의 Iterator 방식을 사용하면 내 IDE 에서는 이 방법으로 바꾸라고 추천해준다.
list.removeIf(str -> str.startsWith("김", 0));
코드 자체는 한 줄 컷이 되어버려 깔끔해진다.
3. Fail-Safe Iteration
Collection 의 사본을 만들어 반복하는 방법이다.
수정이 발생해도 사본은 그대로 유지되어 Collection 에 직접접근해 수정해도 계속 반복된다.
Thread-Safe 하며 List 의 경우는 CopyOnWriteArrayList 클래스를 사용하는데 내부 순회시에는 매우 빠르나 수정 삭제 시 시간과 메모리 소비가 크다고 한다.
List<String> list = new CopyOnWriteArrayList<>();
list.add("김갑환");
list.add("최번개");
list.add("김거한");
list.add("김번개");
list.add("김갑환");
list.add("김갑환");
list.add("장거한");
list.add("장갑환");
list.add("김갑환");
list.add("김김김");
for (String str : list) {
if (str.startsWith("김", 0)) {
list.remove(str);
}
}
// 최번개 장거한 장갑환
System.out.println(Arrays.toString(list.stream().toArray()));
Ref
ConcurrentHashMap, CopyOnWriteArrayList에 대하여
0.들어가기전 ArrayList와 HashMap을 Thread safe하게 사용 하는 방법은 무엇인가?에 대한 대답으로 아래의 내용을 공부 해 보았다 1.Thread Safe 란? 멀티 쓰레드 환경에서 개발자가 의도한대로 코드가 동작
goneoneill.tistory.com
https://www.baeldung.com/java-fail-safe-vs-fail-fast-iterator
나중에 확인해볼 Set Fail-Safe 방법 활용 시 구현 클래스들 두가지에 대한 차이
Different types of thread-safe Sets in Java
There seems to be a lot of different implementations and ways to generate thread-safe Sets in Java. Some examples include 1) CopyOnWriteArraySet 2) Collections.synchronizedSet(Set set) 3)
stackoverflow.com
'Java' 카테고리의 다른 글
Annotation (0) | 2022.02.20 |
---|---|
[java] Reflection (0) | 2022.02.14 |
[Java] 올바른 Map Iteration (0) | 2022.01.23 |
[Java] 예외란 뭘까 (0) | 2022.01.10 |
[Java] Abstract , Interface (0) | 2022.01.09 |