본문 바로가기
BE/Java

[JAVA] ConcurrentHashMap은 뭘까? (synchronizedMap, HashMap과의 비교)

by suhyeon chae 2024. 6. 3.

 

서론

여러 강의나 예제를 보면 동시성을 생각하면 concurrentHashMap을 사용하라고 한다. 왜일까? 그리고 도대체 뭘까? 

 

ConcurrentHashMap 이란?!

- Java의 java.util.concurrent 패키지에 포함된 동시성 map

- 여러 스레드가 동시에 데이터를 안전하게 읽고 쓸 수 있도록 설계된 자료구조

- 기본적으로 동시성 제어를 위해 여러 메커니즘을 사용하여 성능 저하 없이 스레드 안전성을 제공

 

ConcurrentHashMap의 특징과 동작 방식

1. 세분화된 잠금

  - ConcurrentHashMap은 내부적으로 세그먼트(segment)나 버킷(bucket) 수준에서 잠금을 관리. 이는 전체 맵이 아닌 일부에만 잠금을 걸어 여러 스레드가 병렬로 작업할 수 있게 함

  - Java8 이후로는 세그먼트 대신 개별 버킷 수준에서 동시성을 제어하는 방식으로 개선됨

 

2. 읽기 작업의 비동기 처리

  - 대부분의 읽기 작업은 잠금을 사용하지 않거나 최소한의 잠금만 사용하여 처리됨. 이는 읽기 작업의 성능을 극대화 함

 

3. 쓰기를 위한 잠금

  - 쓰기 작업은 잠금을 사용하여 데이터 일관성을 유지함. 전체 맵을 잠그지 않고, 특정 버킷이나 노드에 대해서만 잠금을 걸기 때문에 병목 현상을 최소화함

 

4. 리사이징

  - 해시 테이블의 크기가 증가해야할 때 리사이징 작업은 병렬로 수행됨. 이는 전체 맵을 잠그지 않고도 안전하게 크기를 조정할 수 있게 함

ConcurrentHashMap VS HashMap VS synchronizedMap 비교

HashMap

- 비동기적으로 동작하며 멀티스레드 환경에서 안전하지 않음 (동기화 지원x)

- 스레드가 동시에 접근할 경우 데이터의 일관성이 깨질 수 있음

Collections.synchronizedMap

- 내부적으로 모든 접근에 대해 동기화 블록을 사용

- 동시성 제어는 가능하지만, 모든 쓰기 작업과 대부분의 읽기 작업에서 전체 맵 잠금이 발생하여 성능 저하가 심함

ConcurrentHashMap

- 전체 맵 잠금이 아닌 세분화된 잠금을 사용하여 더 많은 스레드가 병렬로 작업할 수 있음

- 읽기 작업은 대부분 잠금을 사용하지 않거나 최소한의 잠금을 사용하여 높은 효율성 제공

 

HashMap과 ConcurrentHashMap 코드 예제

* HashMap 예제 코드 (동시성 문제로 데이터의 일관성 깨짐)

public class HashMapExample {
    private static Map<String, Integer> map = new HashMap<>(); // HashMap 선언

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10); // 10개의 고정된 스레드를 가진 스레드풀 생성

        for (int i = 0; i < 1000; i++) { // 1000번 반복하면서 map.put() 작업을 실행
            int finalI = i;
            executorService.execute(() -> map.put("key" + finalI, finalI));
        }

        executorService.shutdown(); // 더 이상 새로운 작업을 받지 않게 함
        executorService.awaitTermination(1, TimeUnit.MINUTES); // 모든 작업이 완료되거나, 1분이 지나면 종료

        // 예상되는 출력은 1000이지만, 실제로는 일관되지 않은 결과를 출력할 수 있음
        System.out.println("Map size: " + map.size()); // 실행 결과가 매번 달라짐 996, 975, 992 ... 
    }
}

1000이 아닌 실행할 때마다 다른 개수로 나옴

 

 

* ConcurrentHashMap 예제 코드 (동시성 문제 해결)

public class ConcurrentHashMapExample {
    private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // ConcurrentHashMap 선언

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10); // 10개의 고정된 스레드를 가진 스레드풀 생성

        for (int i = 0; i < 1000; i++) { // 1000번 반복하면서 map.put() 작업을 실행
            int finalI = i;
            executorService.execute(() -> map.put("key" + finalI, finalI));
        }

        executorService.shutdown(); // 더 이상 새로운 작업을 받지 않게 함
        executorService.awaitTermination(1, TimeUnit.MINUTES); // 모든 작업이 완료되거나, 1분이 지나면 종료

        // ConcurrentHashMap을 사용하면 예상대로 1000이 출력.
        System.out.println("Map size: " + map.size());
    }
}

1000이 나옴