멀티 스레드를 활용한 서비스 개발을 하던 중, 해당 개념을 알게 되었고 이게 무엇인지에 대해 상세하게 다룰 필요가 있다고 생각해 작성했다.
정의
ConcurrentHashMap은 Java에서 동시성 문제를 해결하기 위해 설계된 스레드 안전한 HashMap이다. Java 1.5부터 java.util.concurrent 패키지에 포함되어 있으며, 여러 스레드가 동시에 읽고 쓸 수 있도록 특별한 락 분할 메커니즘을 사용한다.
만들어진 배경
기존 HashMap은 멀티스레드 환경에서 동기화가 되어 있지 않아 race condition이 발생할 수 있음
이를 해결하기 위해 Hashtable이나 synchronizedMap을 사용했지만, 이들은 전체 맵에 대해 synchronized 처리를 하여 성능 병목이 생김.
=> 이를 개선하고자 보다 정교하게 동기화를 제어할 수 있는 ConcurrentHashMap이 등장하였다.
race condition: 둘 이상의 스레드가 동시에 같은 자원에 접근하거나 변경하면서, 실행 순서에 따라 결과가 달라지는 문제 상황
특징
세분화된 락(분할 락): Java 8 이전까지는 Segment 단위로 락을 나눴고, Java 8부터는 bucket(배열의 각 요소) 단위로 동기화하여 병렬성을 높였다.
읽기 작업은 락 없이 처리
대부분의 읽기 작업은 락 없이 진행되며, 변경 작업만 최소한의 락으로 처리된다.
null key, null value 금지
null 키나 null 값을 허용하지 않으며, NullPointerException이 발생한다.
성능 중심 설계
락의 범위를 최소화하여 성능 저하 없이 스레드 안전성을 확보했다.
동작 구조
1. 버킷이 비어 있으면 락 없이 넣는다 (CAS)
데이터를 넣을 위치(버킷)가 비어 있으면,
복잡한 락 없이, 한 번에 넣을 수 있는 방법(CAS)을 사용해 데이터를 삽입 CAS(Compare-And-Swap): "현재 값이 예상한 값과 같으면 새로운 값으로 바꿔라!" 라는 조건부 연산
// Java의 AtomicInteger에서 예시
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 현재 값이 0이면 1로 바꿔라`
2. 충돌하면 그 버킷만 잠근다 (synchronized)
같은 버킷에 여러 데이터가 들어가야 할 때(해시 충돌),
전체 맵을 잠그지 않고 그 버킷 하나만 잠근다.
해당 방법으로 병렬 처리 성능 유지
3. 충돌이 많아지면 연결 리스트를 트리로 바꾼다
같은 버킷에 데이터가 너무 많이 몰리면
단순히 줄 세우는 연결 리스트보다, 검색 빠른 트리(Red-Black Tree)로 바꿔서
성능 저하를 막는다.
장점과 단점
장점
높은 동시성 처리: 여러 스레드가 동시에 읽기/쓰기 가능
Deadlock 방지: 전체 맵을 잠그지 않기 때문에 병목이 적음
신뢰성 있는 동시 처리: 락이 세분화되어 있어 race condition 방지에 효과적
단점
null 키/값 불가: 기존 HashMap과 달리 null 관련 유연성이 없음
복잡한 내부 구현: 구조가 복잡해 디버깅이나 커스터마이징이 어려움
특정 상황에서의 성능 저하: 높은 충돌률 시 segment 간 contention 발생 가능 contention(락 경쟁, 경합): 여러 스레드가 한개 버킷에 접근할 때 버킷의 락이 병목 지점이 돼서 여러 스레드가 대기 상태에 빠지는 상황