본문으로 건너뛰기
김신건의 로그

[Java] ConcurrentModificationException

· 수정 · 📖 약 2분 · 691자/단어 #java #exception #iterator #concurrency #fail-fast
ConcurrentModificationException, CME, java.util.ConcurrentModificationException, ConcurrentModification, CMException

정의

java.util.ConcurrentModificationException (CME) 는 fail-fast iterator컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 (RuntimeException).

이름과 달리 반드시 동시성 (멀티스레드) 문제일 때만 발생하는 것이 아니다. 단일 스레드에서도 자기 자신을 잘못 수정하면 발생한다.

java.util 의 거의 모든 컬렉션 (ArrayList, LinkedList, Vector, HashMap, TreeMap, HashSet, TreeSet) 의 iterator 가 던질 수 있다.

가장 흔한 두 시나리오

1. 순회 중 자기 수정 (단일 스레드)

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));

for (Integer x : list) {           // iterator 사용
    if (x % 2 == 0) {
        list.remove(x);            // ❌ CME 던짐
    }
}

for-each 는 내부적으로 iterator() 를 호출. 그 iterator 가 만들어진 뒤 list.remove()modCount 를 증가시킨다. 다음 it.next() 에서 검사 실패 → CME.

2. 멀티스레드 동시 수정

List<Integer> shared = new ArrayList<>();
// Thread A
for (Integer x : shared) { /* 읽기 */ }
// Thread B (동시 실행)
shared.add(99);  // ← Thread A 의 다음 next() 에서 CME

이 경우 CME 는 best-effort 신호. CME 가 안 나도 안전한 것은 아니다 (더 무서운 [silent corruption] 가능).

메시지에 없는 정보

CME 의 메시지에는 누가 수정했는지 가 적혀 있지 않다.

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
    at MyApp.main(MyApp.java:23)

Itr.next() 가 trigger 일 뿐, 진짜 원인은 그 사이에 일어난 다른 어딘가의 수정. 디버깅을 위해서는:

  1. iterator 와 같은 스택 위쪽 코드 부터 살핀다. 같은 메서드 안에 add/remove 가 있는지.
  2. 공유 컬렉션이라면 다른 스레드의 호출 을 추적한다. 보통 lambda, async, scheduled task.
  3. ArrayList 등에 디버거를 걸어 modCount 변화를 추적 한다.

해결 패턴

Iterator.remove() 사용

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    Integer x = it.next();
    if (x % 2 == 0) {
        it.remove();   // ✓ iterator 가 expectedModCount 도 함께 갱신
    }
}

Iterator.remove() 는 컬렉션의 remove() 를 호출한 뒤 expectedModCount = modCount 로 동기화한다. 그래서 CME 가 발생하지 않는다.

removeIf 사용 (Java 8+)

list.removeIf(x -> x % 2 == 0);

내부적으로 Iterator.remove() 와 같은 안전한 방식. 더 짧고 명확.

복사본 순회

for (Integer x : new ArrayList<>(list)) {   // 복사본 순회
    if (x % 2 == 0) {
        list.remove(x);                     // ✓ 원본은 수정
    }
}

복사 비용을 감수하는 대신 가장 단순한 해결.

동시성 컬렉션 사용

진짜 멀티스레드 환경이라면 fail-fast 컬렉션 자체를 피한다.

// 읽기 많음, 쓰기 적음
List<Integer> list = new CopyOnWriteArrayList<>();

// 키-값
Map<String, Integer> map = new ConcurrentHashMap<>();

// 큐
Queue<Task> q = new ConcurrentLinkedQueue<>();

이들은 CME 를 던지지 않으면서 동시 수정/순회를 지원한다.

잡으면 안 된다

// ❌ 안 좋은 패턴
try {
    for (Integer x : list) {
        if (cond) list.remove(x);
    }
} catch (ConcurrentModificationException e) {
    // 무시
}

이 catch 는 버그를 숨길 뿐. iterator 가 일관성 없는 상태로 더 진행하지 않은 것은 다행이지만, 그래도 “왜 동시 수정이 일어났는가” 라는 진짜 문제는 그대로다.

IMPORTANT

CME 는 잡지 말고 고쳐라. “왜 이게 던져졌는가” 라는 질문에서 출발해 코드의 동시성 가정을 다시 본다. CME 가 사라지도록 위의 해결 패턴 중 하나를 적용.

자주 만나는 추가 사례

Stream.forEach 도 동일

list.stream().forEach(x -> {
    if (cond) list.remove(x);  // ❌ CME
});

내부적으로 iterator 사용. 같은 함정.

HashMap 순회 중 put

Map<String, Integer> map = new HashMap<>();
for (var entry : map.entrySet()) {
    map.put("new", 1);   // ❌ CME (key 가 새로 추가되면 구조 변경)
}

값만 바꾸는 것 (entry.setValue(...)) 은 OK.

nested loop

for (Integer a : listA) {
    for (Integer b : listB) {
        if (cond) listA.remove(a);  // ❌ outer 의 CME
    }
}

자주 놓치는 케이스, outer loop 에 영향.

참고

이 글의 용어 (5개)
[Java] ArrayListjava
정의 는 동적 배열 (dynamic array) 로 구현된 . 내부적으로 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다. Java 의 가장 흔한 컬렉션 구현체. 인덱스 기…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] LinkedListjava
정의 는 양방향 연결 리스트 (doubly linked list) 로 구현된 이자 . 각 원소는 와 포인터를 가진 노드로 보관된다. 와 같은 List 이지만, 메모리 레이아웃과 …
Geometry Basic: 벡터, CCW, 외적, 내적algorithm
정의 기하 알고리즘의 밑바탕. 대부분의 2D/3D 기하 문제는 벡터 연산, 내적/외적, CCW 판정 세 가지의 조합으로 풀립니다. 벡터 2D 벡터: 두 점 $A = (ax, ay…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기