ConcurrentHashMap

1. ConcurrentHashMap이란?

ConcurrentHashMap은 멀티스레드 환경에서 동시에 여러 스레드가 안전하고 빠르게 데이터를 읽고 쓸 수 있도록 설계된 해시맵입니다.
기존의 HashMap은 스레드에 안전하지 않아 동시 접근 시 데이터 손상이나 무한 루프 같은 심각한 문제가 발생하지만, ConcurrentHashMap은 이를 해결했습니다.


2. ConcurrentHashMap 동작 원리

2-1. 세분화된 락 (Lock Striping)

자바 7 이전에는 ConcurrentHashMap이 내부적으로 데이터를 여러 세그먼트(segment)로 나누고, 각 세그먼트별로 락을 걸어 동시 접근을 허용했습니다.
이 덕분에 전체 맵이 아닌 해당 세그먼트에만 락이 걸려, 동시성 성능이 크게 향상됐죠.

2-2. 자바 8 이후: 락 대신 CAS + 버킷별 동기화

자바 8부터는 내부 구조가 크게 바뀌어, CAS(Compare-And-Swap)라는 하드웨어 원자 연산과 버킷 단위의 락만 사용합니다.

  • CAS는 락(lock)을 쓰지 않는(non-blocking), 원자적(atomic) 연산 방식의 하나입니다.
  • CAS는 CPU 명령어 수준에서 “현재 값이 예상값(oldValue)과 같으면 새 값(newValue)으로 바꿔라”를 한 번에 처리하는 원자적 연산입니다. (낙관적 동시성)
  • 만약 예상값과 메모리에 있는 실제 값이 다르면, 교체를 하지 않고 연산은 실패로 돌아가며 보통 다시 시도(retry)합니다.
  • 하드웨어나 OS/CPU 명령 수준에서 지원하는 경우가 많고, Java는 Unsafe나 JVM 내부 + Atomic* 클래스들을 통해 CAS를 제공합니다.
  • 이 덕분에 락 없이도 값을 안전하게 갱신할 수 있어, 락 경쟁을 줄이고 성능을 극대화했습니다.

 

2-2-1. ConcurrentHashMap이 락 없이 원자적으로 동작하는 원리

1. CAS의 하드웨어 수준 원자적 연산

  • CPU 명령어(CPU atomic instruction)
    CAS는 CPU가 직접 지원하는 원자적 명령어입니다. 예를 들어 x86 계열의 CMPXCHG 명령어가 대표적이죠.
  • 동작 원리
    CPU는 메모리 특정 위치에 대해 "현재 값이 예상값이면, 새 값으로 교체"를 한 사이클 내에 수행합니다. 이 사이에 다른 스레드가 개입할 수 없으니, 완전한 원자성을 보장합니다.
  • 특징
    • 하드웨어가 직접 지원하므로 매우 빠르고 신뢰성이 높음
    • JVM이나 OS 커널에서 별도의 락을 걸 필요 없음
    • 멀티코어 환경에서도 메모리 동기화를 하드웨어 수준에서 처리

2. Java에서 CAS를 제공하는 방식

자바는 직접 하드웨어 명령어를 호출하지 않고도 CAS를 쓸 수 있게 여러 추상화 계층을 둡니다.

  • sun.misc.Unsafe 클래스
    • JVM 내부에 존재하는 비공개(low-level) API
    • 하드웨어 CAS 명령어를 JNI나 JVM 내에서 호출하도록 연결해줌
    • 직접 메모리 접근과 CAS 같은 원자적 연산을 가능하게 함
    • Java 9부터는 공식 API가 아닌 jdk.internal.misc.Unsafe로 이전되고 사용이 제한적임
  • java.util.concurrent.atomic 패키지 (AtomicInteger, AtomicReference 등)
    • Unsafe API를 감싸 사용하기 쉽게 만든 고수준 클래스
    • 내부에서 Unsafe의 CAS 기능을 호출해 원자적 연산 수행
    • 멀티스레드 환경에서 락 없이 안전한 동시성 제어 지원
  • JVM 내부 역할
    • JVM은 Unsafe를 통해 하드웨어 CAS 명령어와 자바 객체 메모리를 연결하는 역할 수행
    • JIT(Just-In-Time) 컴파일러가 CAS 연산을 CPU 명령어로 최적화

3. ConcurrentHashMap이 사용하는 CAS 방식

  • ConcurrentHashMap은 자바 8 이상에서 내부적으로 Unsafe와 Atomic* 클래스를 조합해 CAS 연산을 수행합니다.
  • 구체적으로는:
    • 내부 구조인 버킷 배열 요소노드의 값 참조에 대해 Unsafe의 CAS 메서드 (compareAndSwapObject 등)를 호출해 원자적 갱신을 처리합니다.
    • 별도의 락 없이 값 업데이트를 시도하고, 실패 시 재시도하는 방식으로 동시성을 보장합니다.
  • 즉, 하드웨어 수준의 원자적 명령어를 JVM이 Unsafe를 통해 호출하고, ConcurrentHashMap이 이를 활용하는 구조입니다.
  • 이 덕분에 락을 최소화하면서도 스레드 안전한 동시성 구현이 가능합니다.

 

2-2-2. CAS 사용 장단점 및 문제점

장점

  • 락을 안 쓰므로, 데드락(lock deadlock) 같은 위험이 없고, 락 대기(wait) 비용도 없음.
  • 경쟁(충돌)이 적을 경우 매우 빠르게 동작함.
  • 주로 락-프리(lock-free) 혹은 낙관적 동시성(optimistic concurrency) 방식에 쓰임.

단점 / 주의점

  • 재시도 비용
      CAS가 실패하면(즉, 예상값과 실제값이 다르면), 보통 루프를 돌면서 재시도를 해야 함 → 바쁜 대기(spin)가 반복될 수 있음.
  • ABA 문제
      예: 어떤 위치의 값이 A → B → A 로 바뀌었다면, 처음 A와 최종 A는 같아 보이므로 CAS가 “아직도 A였구나” 하고 허락해 버릴 수 있음. A → B → A 과정을 무시한다는 뜻.
      이를 막기 위해 버전 태그(version tag)나 포인터 + 버전 정보를 같이 갖는 방식 등이 사용되기도 함.
  • 복잡한 연산에는 부적합
      여러 필드를 동시에 바꿔야 한다거나 복잡한 구조 변경이 필요하면, CAS만으로 처리하기 어렵고 락이나 다른 동기화 기법이 필요해짐.

3. 충돌이 많아지는 버킷: 연결 리스트에서 트리 구조로 변화

기본적으로 해시맵은 같은 해시값을 가진 키들이 버킷 안에서 연결 리스트 형태로 저장됩니다.
하지만 충돌이 많이 발생해 연결 리스트가 길어지면, 조회 성능이 선형적으로 느려집니다.

ConcurrentHashMap은 연결 리스트가 일정 길이(기본 8개) 이상이 되면 이를 빨간-검정 트리(Red-Black Tree) 구조로 바꿔 조회 성능을 로그 시간(log n)으로 최적화합니다.
이 변화는 자바 8부터 적용된 최적화입니다.


4. CAS가 락 없이 원자적으로 동작하는 이유

CAS 연산은 CPU의 특수 명령어(CMPXCHG)를 사용해, 메모리의 특정 위치 값을 예상값과 비교 후 교체하는 작업을 하나의 원자적 명령어로 수행합니다.
이 과정은 하드웨어 레벨에서 처리되므로, 별도의 락 없이도 안전하게 값을 업데이트할 수 있습니다.

즉, 여러 스레드가 동시에 같은 키에 put을 시도해도, CAS 연산이 성공할 때까지 재시도하는 방식으로 안전성을 확보하는 것이죠.


5. ConcurrentHashMap에서 Lock과 Get 요청은 어떻게 공존하나?

put 작업 중에 락이 걸린 상태라도, 다른 스레드의 get 요청은 락에 방해받지 않고 바로 처리됩니다.
이는 ConcurrentHashMap이 내부적으로 락을 걸더라도, 읽기 작업은 락 없이도 일관성 있게 값을 조회할 수 있도록 설계되었기 때문입니다.
그래서 쓰기 락이 있어도 읽기 성능은 거의 저하되지 않습니다.


6. ConcurrentHashMap 주요 특징 정리

항목 설명
동시성 제어 방식 CAS + 버킷 단위 락 (자바 8 이상)
내부 데이터 구조 배열 + 연결 리스트 + 트리(Red-Black Tree)로 충돌 대응
읽기 작업 성능 락 없이 처리, 매우 빠름
쓰기 작업 성능 버킷 단위 락 사용, 락 범위가 좁아 병목 최소화
충돌 처리 일정 개수 이상 충돌 시 연결 리스트 → 트리 구조로 변경
주요 메서드 put(), get(), remove(), computeIfAbsent()
활용 예 웹 세션 관리, 캐시 구현, 멀티스레드 데이터 공유