들어가기 전
이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.
이 글의 요약
- ConcurrentModificationException은 멀티 쓰레드 환경에서 변하면 안되는데 변한 조건들이 있을 때 발생하는 에러다. 예를 들면 syncronzied 메서드 내에서 다른 메서드가 변수를 수정한다거나 했을 때 발생한다.
- ConcurrentModificiationException은 싱글 쓰레드에서도 발생하는데 주로 fail-fast를 기반으로 한 Collection을 순회하면서, element를 변경했을 때 발생한다.
완벽 공략 38. ConcurrentModificationException
ConcurrentModificationException은 현재 바뀌면 안되는 것을 수정할 때 발생하는 예외다. 이 Modification은 이름과는 다르게 멀티 스레드 뿐만 아니라 싱글 스레드 상황에서도 발생할 수 있다. 가장 대표적인 예시로는 fail-fast Iterator를 사용해 Collection을 순회하고 있는 상황에서 Collection을 변경하는 경우에 발생한다.
어떤 한 스레드가 Collection을 순회하고 있는데, 다른 쓰레드가 Collection을 변경하려 한다면 순회한 결과가 예측이 불가능해진다. 그래서 fail-fast Iterator 라는 개념이 도입되었는데, 이런 상황이 감지되면 ConcurrentModificationException을 던져서 '빨리 실패'하게 한다.
fail-fast Iterator는 Collection을 도는 도중에 '이런 변경점'이 발생하면 바로 예외를 던진다. 나중에 이상한 것을 감지하는 리스크를 감수하지 않고, 문제가 발생한 시점에 바로 예외를 던져서 더 큰 문제의 발생을 원천 차단한다.
List.of() / ArrayList 의 차이점
Collection 중에는 List.of()와 ArrayList가 있다. 그런데 각각은 다르게 동작한다. List.of()로 생성하는 List는 ImmutableCollections 클래스다. 이것은 수정이 불가능한 Collection을 의미한다. 따라서 초기에 한번 생성되고 난 다음에는 수정 (추가 / 삭제)등이 불가능하다.
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5); // immutableCollection임
numbers.remove(1);
}
// List.java
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5);
}
만약 List.of()로 생성된 List에 수정 작업을 할 경우 UnsupportOperationException을 던져준다.
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:71)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:75)
at com.example.effectivejava1.chapter22.failfast.FailFast.main(FailFast.java:10)
반면 ArrayList는 얼마든지 수정이 가능한 클래스다. 따라서 ArrayList를 생성하고 배열에 값을 넣고 뺄 수 있다.
ArrayList에서 수정 시 ConcurrentModificationException 발생
아래 코드를 실행시켜보면, ConcurrentModificationException이 발생한다.
- numberList에서 Iteration을 하게 되면, ListItr 객체가 반환된다.
- ListItr 객체는 fast-fail Iterator이기 때문에 Iteration 도중에 변경이 감지되면 ConcurrentModificationException을 발생시킨다.
ArrayList<Integer> numberList = new ArrayList<>();
numberList.add(1);
numberList.add(2);
numberList.add(3);
numberList.add(4);
numberList.add(5);
// 이터레이션으로 콜렉션을 순회하는 중에 Collection의 remove를 사용하면,
// ConcurrentModificationException 발생
for (Integer number : numberList) {
if (number == 3) {
numberList.remove(number);
}
}
아래에서 볼 수 있듯이 listIterator()는 ListIterator 인스턴스를 반환한다. 이 인스턴스는 fast-fail Iterator 이기 때문에 동작 도중에 변경이 감지되면 ConcurrentModification이 발생한다.
ArrayList.java
// fail-fast Iterator를 반환시킨다.
public ListIterator<E> listIterator(int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
Collection을 순회하면서 삭제하고 싶다면?
일반적으로 Iteration을 돌면서 Element를 삭제한다면, fast-fail Iterator에 의해서 ConcurrentModificationException이 발생할 수 있다. 이 에러가 발생하지 않고 Collection을 순회하면서 삭제할 때는 다음 방법을 사용할 수 있다.
- Iterator를 직접 사용해서 제거를 한다.
- Index를 기준으로 for문을 돌며 제거한다.
- removeIf를 이용해서 제거한다. (내부적으로 Iterator를 직접 사용하는 것이다.)
아래에 각 코드의 예시가 있다. 가장 좋은 방법은 removeIf를 이용해서 제거하는 것이다. 코드의 가독성이 가장 좋기 때문이다.
// 이터레이터의 remove 사용하기
// Exception 발생 X
for (Iterator<Integer> iterator = numberList.iterator(); iterator.hasNext();) {
Integer integer = iterator.next();
if (integer == 3) {
iterator.remove();
}
}
// 인덱스 사용하기
// Exception 발생 X
for (int i = 0; i < numberList.size(); i++) {
if (numberList.get(i) == 3) {
numberList.remove(numberList.get(i));
}
}
// removeIf 사용하기
// 내부적으로 Iterator를 직접 사용하는 것과 동일
// Exception 발생 X
numberList.removeIf(integer -> integer == 3);
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템23. 태그 달린 클래스보다는 클래스 계층 구조를 활용하라. (0) | 2023.04.12 |
---|---|
Effective Java : 아이템22. 인터페이스는 타입을 정의하는 용도로만 사용하라. (0) | 2023.04.12 |
Effective Java : 아이템21. 인터페이스는 구현하는 쪽을 생각해 설계하라. (0) | 2023.04.12 |
Effective Java : 아이템20. 완벽 공략 (0) | 2023.04.12 |
Effective Java : 아이템20. 추상 클래스보다 인터페이스를 우선하라. (0) | 2023.04.12 |