ConcurrentModificationException과 for문 성능 개선

DongHyun Kim·2024년 8월 29일
0
post-thumbnail
post-custom-banner

컬렉션 객체를 순회하면서 특정 값을 삭제하고 싶을 때 어떻게 처리할까

동시성 문제?

  • 컬렉션 객체에서 특정 값을 삭제하고 싶을 때 발생했던 런타임 문제
  • 향상된 for문
    • ConcurrentModificationException 문제가 발생한다

왜 동시성 문제일까? class 파일을 분석해보자

  • 향상된 for문 문법을 포함하는 class 파일

    위 코드에서 향상된 for문이 Iterator 객체로 호출되어 while에서 hasNext() 로 바뀐 로직을 볼 수 있다

     그런데 remove를 호출하는 부분에서 루프를 돌고있는 객체에서 삭제하는 부분이 있다. 이 부분이 문제가 되어 동시성 문제가 발생했을 것이라 예측하고, Iterator와 remove(obj) 부분을 더 살표보자

    [iterator] 를 살펴보자

  • ArrayList에서 iterator()는 내부클래스인 Itr를 반환한다

  • Itr 클래스는 클래스 변수로 리스트의 데이터 변경 여부를 체크하고 있다

    • Itr는 ArrayList 내에 선언된 내부 클래스이다
        // ArrayList.class
        public Iterator<E> iterator() {
        	return new Itr();
        }
      private class Itr implements Iterator<E> {
      	int cursor; // next를 호출했을 때 반환할 element의 index
      	int lastRet = -1; // 마지막 element의 index
      	int exepctedModCount = modCount; // List가 수정된 횟수
      	
      	Itr() {}
      	...
      }
  • Iterator의 remove는 ArrayList에 구현되어있고, fastRemove를 호출해서 삭제한다

    • fastRemove는 modCount를 변형시킨다


    • Iterator의 next() 메서드로 다음 element를 가져오려고 시도할 때 modCount 변환 여부를 체크하여 에러가 발생한다

checkForComodification()에서 modCount가 변한 여부를 체크한다

  • 위와 같은 이유로 remove(int index) 나 remove(Object obj) 방식 대신에 iterator에 선언된 remove() 함수로 원본 Collection에 지장없이 삭제를 해야한다

동시성 이슈이므로 Concurrent 패키지로 해결도 가능하다

  • CopyOnWriteArrayList 의 remove

ConcurrentModificationException은 개발자의 실수로 발생할 수 있다. 그러므로 회피가 아닌 다른 방법을 찾아보자

  • 순회할 객체를 불변 객체인 unmodifiableList 로 감싸자
  • 그리고 불변객체에서 원하는 값만 추출하거나 제외해서 새로운 객체를 만드는 filter 메서드를 사용하자

for문의 처리 성능을 높이고 싶다면

  • 사실 멀티쓰레드를 사용했을 때 하나의 객체를 동시에 접근하는 것은 동시성 이슈에서 자유롭지 못하다.
  • 그러므로 위에서 고려한 것을 조합해서 for문과 멀티쓰레드를 2가지 방식으로 설계해봤다
  • ExcutorService & synchronized
@Test
void enhancedForLoop(){
    ExecutorService executor = Executors.newFixedThreadPool(4);
    List<String> copyTarget = new CopyOnWriteArrayList<>(targetList);

    for (String str : copyTarget) {
        executor.submit(() -> {
            if (str.equals("a")) {
                synchronized (copyTarget) {
                    copyTarget.remove(str);
                }
            }
        });
    }
    executor.shutdown(); // 새로운 작업을 받아들이지 않고, 이미 실행된 작업들을 모두 실행한 후 종료
    while (!executor.isTerminated()) {
        // 모든 작업이 끝날 때까지 기다림
    }
    assertThat(copyTarget.size()).isEqualTo(300000);
}
  • parallelStream
@Test
void enhancedUnmodifiableForLoop() {
    long startTime = System.currentTimeMillis();
    List<String> unmodifiableList = Collections.unmodifiableList(targetList);
    List<String> afterFilter = unmodifiableList.parallelStream().filter(str -> {
//            System.out.println(Thread.currentThread().getName() + " processed " + str);
        if(str.equals("a")){
            return false;
        }
        return true;
    }).toList();
    assertThat(afterFilter.size()).isEqualTo(300000);
    long endTime = System.currentTimeMillis();
    System.out.println("Execution time: " + (endTime - startTime) + " ms");
}
  • 테스트는 4만건의 String List에서 For문을 돌며 1만건의 String을 삭제하는 작업이었다.

iterator의 remove() 로 처리한 결과는 2993ms 였고

parallelStream으로 처리한 결과 71ms 로

결과적으로 97% 성능을 향상시켰다

profile
do programming yourself
post-custom-banner

0개의 댓글