Skip to the content.

TreeMap의 멀티스레드 환경에서의 문제와 해결 방법

회사 프로젝트에서 멀티스레드 환경에서 TreeMap을 이용하여 기본값을 설정하고, 요청이 오면 해당 값을 확인한 후 TreeMap에 값이 없으면 해당 값을 넣고 가져오는 방식의 로직을 사용했습니다. 그러나 첫 번째 요청에서 새로운 값(예: 1)을 넣고, 두 번째 요청에서 첫 번째 요청에서 넣었던 값을 가져오려고 할 때 NullPointerException(NPE)이 발생하는 이슈가 있었습니다. 이 문제를 재현하고 해결 방법을 찾기 위해 테스트 코드를 작성해보았습니다.

테스트 코드(출처: chatGPT)

void test_3() {
    ExecutorService executorService = Executors.newFixedThreadPool(20);

    // Runnable to put a value
    Runnable put = () -> {
        for (int i = 0; i < 100; i++) {
            log.error("put: image/jpeg: {}, JPEG: {}", i, i);
            mapByMimeType.put("image/jpeg" + i, "JPEG" + i);
        }
    };

    // Runnable to get a value
    Runnable get = () -> {
        for (int i = 0; i < 100; i++) {
            String value = mapByMimeType.get("image/jpeg" + i);
            log.error("get: image/jpeg: {}, {}", i, value);
        }
    };

    // Submitting 10 put tasks
    for (int i = 0; i < 10; i++) {
        executorService.submit(put);
    }

    // Submitting 10 get tasks
    for (int i = 0; i < 10; i++) {
        executorService.submit(get);
    }

    executorService.shutdown();
    try {
        if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
            executorService.shutdownNow();
        }
    } catch (InterruptedException e) {
        executorService.shutdownNow();
        log.error("Exception: {}", e.getMessage());
    }

    System.out.println("Finished without exception");
}

테스트 결과

특정 번호의 작업 로그를 확인해보았습니다:

• 875번줄: 55번 작업이 먼저 get을 동작했는데 아직 put하지 않았기 때문에 null이 뜨는 것은 정상입니다.

• 986번줄: 55번 값을 TreeMap에 넣었습니다 (put 작업 완료).

• 1027번줄: 위에서 값을 넣었음에도 null이 뜨는 현상이 발생했습니다.

원인 분석

TreeMap은 레드-블랙 트리(Red-Black Tree)를 기반으로 하고 있으며, 삽입 또는 삭제 시 트리의 균형을 유지하기 위해 내부적으로 회전 연산(rotate)을 수행합니다. 이 회전 연산 중에 다른 스레드가 get 연산을 수행하면, 회전 중간 상태에서 null 값을 반환하거나 NullPointerException이 발생할 수 있습니다.

정리하자면 TreeMap은 멀티스레드 환경에서 부적합한 Map 컬렉션입니다.

해결 방법

멀티스레드 환경에서 안전하게 Map을 관리하려면 다음과 같은 방법을 사용할 수 있습니다:

  1. 메서드를 synchronized로 선언:

• 각 메서드에 동기화 블록을 추가하여 동시 접근을 제어합니다.

  1. Collections.synchronizedSortedMap 사용

  2. ConcurrentSkipListMap 사용

결론

TreeMap은 멀티스레드 환경에서 안전하지 않기 때문에, 동기화된 컬렉션을 사용하거나 스레드 안전한 컬렉션인 ConcurrentSkipListMap을 사용하여 멀티스레드 환경에서도 안전하게 엔티티를 관리할 수 있습니다.

위의 테스트 결과와 해결 방법을 통해 멀티스레드 환경에서 Map을 안전하게 관리하는 방법을 고려해야 합니다.

추후 위의 컬렉션들에 대해 확인해보자.