Java Collection 프레임워크를 공부하면서 ArrayList를 사용하고 있었다. 그런데, ArrayList 타입인 afterSet 의 홀수만 제거하는 for-each문을 실행해보니 ConcurrentModificationException 예외가 발생했다.
Set<Integer> integerSet = new HashSet<>(integerList);
List<Integer> afterSet = new ArrayList<>(integerSet);
System.out.println("pre: " + afterSet);
afterSet.sort(Comparator.reverseOrder());
System.out.println("aft: " + afterSet);
for (int num: afterSet){
if (num % 2 != 0) {
Integer temp = afterSet.indexOf(num);
afterSet.remove(temp);
}
}
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at Collections.Practice.main(Practice.java:33)
ConcurrentModificationException 예외가 어디서 나는지 찾아보고자 디버깅을 진행했다. 그 결과 홀수가 제거되고 난 후, 다음 배열 원소에 접근하려고 할 때, 예외가 발생하는 것을 확인하였다.
ArrayList가 반복하는 동안 구조적 수정이 이루어질 때, 동시성 문제를 방지하기 위해 예외가 발생한다.
ConcurrentModificationException
객체의 동시 수정이 허용되지 않을 때 발생하는 예외이다. 일반적으로 한 스레드가 컬렉션을 수정하는 동안 다른 스레드가 수정하는 것은 허용되지 않기 때문이다. 공유 자원에 여러 작업이 동시에 진행될 때, 발생할 수 있는 문제를 방지하기 위함이다.
for-each 순회에서 다음 요소에 접근하려고 할 때 ConcurrentModificationException 예외가 발생했는데, 그렇다면 동시성 문제가 발생한 것으로 보아야할까?
List 는 modCount 를 사용하여 인스턴스의 구조적으로 수정된 횟수를 관리한다.
// AbstractList.java
protected transient int modCount = 0;
ArrayList 는 순회 중 인덱스를 이동하는 next() 메소드에서 checkForComodification() 를 사용하여 변경을 감지한다.
// ArrayList.java
public E next() {
checkForComodification();
...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount와 expectedModcount를 비교하여 배열이 구조적으로 변경이 일어났는지 확인한다. modCount : 구조적으로 수정된 횟수expectedModcount : 순회을 시작하며 저장한 수정 횟수즉, 순회 중 구조적인 변경을 일으키는 add()와 remove() 를 사용한다면, 인덱스 이동 과정에서 동시 수정 문제를 확인하고 예외를 발생시킨다.
remove() -> fastRemove() -> modCount 증가 -> checkForComodification() -> ConcurrentModificationException 발생 // ArrayList.java
// remove() -> fashRemove() 호출
private void fastRemove(Object[] es, int i) {
modCount++; // 구조적 수정 횟수 증가
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
처음에 코드를 보았을 때는, for-each 뿐만 아니라 ArrayList의 모든 반복문에서 문제가 발생한다고 생각했다. 하지만 관련 글과 코드를 다시보니 동시성 문제라기보다는 for-each 문의 특징 때문이다.
// Exception이 발생하지 않음.
for (int i = 0; i < afterSet.size(); i++) {
if (afterSet.get(i) % 2 != 0) {
afterSet.remove(afterSet.get(i));
}
}
System.out.println(afterSet);
for-each 실행 시 컬렉션의 Iterator이 생성된다.
- Iterator 는 순회 과정에서 컬렉션의 변경 여부를 감지할 수 있도록 설계되었다.
-> 위에2.2 for-each 의 ConcurrentModificationException 발생 과정에서 설명했던 코드가ArrayList.java안에 구현된private class Itr implements Iterator<E>구현 클래스였다.- Iterator은 '읽기 전용' 으로 컬렉션을 읽는 작업만 가능하고, 구조적 변경(삽입, 삭제, 변경)을 허용하지 않는다
ConcurrentModificationException에 대한 주의와 발생 이유 등이 자세하게 나와있다.결국 기존 ArrayList 를 그대로 사용하며 수정하려 했기 때문에 발생한 문제이다. 결과값을 도출해내기 위한 ArrayList 인스턴스 evenList를 새로 생성하여, 저장하였다.
List<Integer> evenList = new ArrayList<>();
for (int num: afterSet){
if (num % 2 == 0) {
evenList.add(num);
}
}
System.out.println(evenList);
or
for-each 가 아닌 for 문을 사용할 수도 있다. 혹은 Iterator 을 직접 사용할 수 있다.
for (int i = 0; i < afterSet.size(); i++) {
if (afterSet.get(i) % 2 != 0) {
afterSet.remove(afterSet.get(i));
}
}
System.out.println(afterSet);
하나의 스레드가 공유 자원에 대해 작업하고 있을 때, 다른 스레드가 접근할 경우 동시성 문제가 발생할 수 있다.
하지만 for-each 문의 ConcurrentModificationException 발생은 동시성 문제보다는 '읽기 전용'의 Iterator 을 사용하기 때문에 구조적 변경이 일어났을 때, 예외를 발생시킨다.
개선할 점
잘한 점