본문 바로가기
language/java

ConcurrentHashMap 동시성 처리 완벽 이해하기

by 죄니안죄니 2026. 6. 1.
반응형

ConcurrentHashMap 동시성 처리 완벽 이해하기

이전 글에서:

  • HashMap
  • synchronized
  • CAS
  • Atomic
  • Thread-safe Collection

을 배웠다.

이번에는 면접에서 정말 자주 나오는:

ConcurrentHashMap은 어떻게 Thread-Safe를 보장하나요?

를 설명한다.

특히 중급 이상 면접에서는:

HashMap과 ConcurrentHashMap 차이
ConcurrentHashMap 내부 구조
CAS 사용 위치
synchronized 사용 위치
 

까지 자주 물어본다.


1. HashMap부터 다시 보자

 
Map<String, String> map =
        new HashMap<>();
 

put()

 
map.put("A", "1");
 

동작.

Hash 계산
 ↓
Bucket 찾기
 ↓
Node 추가
 

2. 문제

Thread A

 
map.put("A", "1");
 

Thread B

 
map.put("B", "2");
 

동시에 실행.


3. 위험

Node 연결 깨짐

데이터 유실

조회 오류
 

가능.


4. 그래서 예전에는

Hashtable 사용.


 
Map<String, String> map =
        new Hashtable<>();
 

5. 내부

 
public synchronized V put(...)
 

모든 메서드.

synchronized
 

6. 문제

Thread A

 
put()
 

Thread B

 
get()
 

Thread C

 
remove()
 

전부 대기.


7. 결과

동시성 낮음
 

성능 나쁨.


8. 그래서 등장

ConcurrentHashMap


9. 목표

Thread-Safe

+

고성능
 

둘 다 달성.


10. JDK 7 구조

예전 구조.


ConcurrentHashMap

 ├─ Segment1
 ├─ Segment2
 ├─ Segment3
 └─ Segment4
 

11. Segment란?

작은 HashMap.


각 Segment가

 
ReentrantLock
 

보유.


12. 장점

Segment1 Lock

↓

Segment2 영향 없음
 

부분 Lock 가능.


13. 하지만

JDK 8부터 제거.


현재는 완전히 다른 구조.


14. JDK 8 구조

현재 기준.


Node[]
 

기반.


HashMap과 비슷.


 
transient volatile Node<K,V>[] table;
 

15. 핵심

면접 단골.


JDK 8 ConcurrentHashMap

CAS

+

synchronized
 

조합.

16. 왜 둘 다 사용할까?

CAS만 사용하면

경쟁 심할 때 비효율.


synchronized만 사용하면

성능 나쁨.


그래서 혼합.


17. put() 흐름

매우 중요.


예:

 
map.put("A", "1");
 

18. Bucket 비어 있음

table[5]

↓

null
 

19. 이 경우

CAS 사용.


null

↓

새 Node
 

원자적으로 삽입.


20. Lock 없음

CAS 성공

↓

끝
 

매우 빠름.


21. Bucket 이미 존재

예:

table[5]

↓

Node 존재
 

22. 이 경우

 
synchronized(firstNode)
 

사용.


23. 왜?

연결 리스트 수정 필요.


Node 연결

Tree 변환

값 변경
 

복합 작업.


CAS만으로 어려움.


24. 중요한 특징

전체 Lock 안 함.


Bucket 하나만 Lock
 

25. 그림

Bucket1
Bucket2
Bucket3
Bucket4
 

Thread A

Bucket1 수정
 

Thread B

Bucket4 수정
 

동시 수행 가능.


26. 그래서 빠름

Hashtable

전체 Lock
 

ConcurrentHashMap

부분 Lock
 

27. get()은?

엄청 중요.


대부분

Lock 없음
 

28. 왜 가능?

Node 필드.


 
volatile V value;
 

 
volatile Node next;
 

가시성 보장.


29. 그래서

 
map.get(...)
 

거의 Lock-Free.


30. resize 문제

HashMap에서 가장 위험한 부분.


배열 확장.

16

↓

32
 

31. ConcurrentHashMap

resize도 병렬 처리.


Thread 여러 개가

데이터 재배치
 

도움.


32. TreeBin 등장

HashMap 글 기억.


JDK 8부터.

충돌 많음
 

LinkedList

Red-Black Tree

변환.


33. ConcurrentHashMap도 동일

Node
 ↓
TreeNode
 

변환.


34. 기준

8개 이상 충돌
 

트리화.


35. TreeBin

트리 관리 객체.


내부에서

Lock
 

관리.


36. size() 문제

생각보다 어려움.


예:

 
map.size();
 

동시에

 
put()
remove()
 

중.


정확한가?


37. 해결

내부 Counter 사용.


CAS 기반.


필요 시 재계산.


38. computeIfAbsent()

실무 중요.


예:

 
cache.computeIfAbsent(
    key,
    k -> loadData(k)
);
 

39. 의미

없으면 생성

있으면 반환
 

Thread-Safe.


40. 실무에서 많이 사용

캐시.


 
ConcurrentHashMap
 
 
computeIfAbsent()
 

조합.


41. 내부 구조 그림

ConcurrentHashMap

table[]

 ├─ Bucket1
 ├─ Bucket2
 ├─ Bucket3
 └─ Bucket4

Bucket 수정

↓

CAS

또는

synchronized
 

42. HashMap vs Hashtable vs ConcurrentHashMap

항목HashMapHashtableConcurrentHashMap
Thread-Safe X O O
전체 Lock X O X
CAS X X O
부분 Lock X X O
성능 좋음 나쁨 매우 좋음

43. 면접 단골 질문

Q. ConcurrentHashMap은 Thread-Safe인가?

 

Q. JDK 8 이후 내부 구조는?

CAS + synchronized
 

Q. get()은 Lock을 사용하는가?

대부분 사용 안 함
 

Q. put()은 언제 synchronized 사용?

Bucket 충돌 시
 

Q. 왜 Hashtable보다 빠른가?

전체 Lock 안 함

Bucket 단위 Lock
 

Q. computeIfAbsent() 장점은?

생성과 조회를
원자적으로 수행
 

44. 실무 예시

Spring 서비스 캐시.

 
private final Map<Long, User> cache =
        new ConcurrentHashMap<>();
 

 
public User getUser(Long id) {

    return cache.computeIfAbsent(
        id,
        userRepository::findById
    );
}
 

멀티스레드에서도 안전.


45. 지금까지 흐름 연결

synchronized
 ↓

volatile
 ↓

CAS
 ↓

Atomic
 ↓

Thread-safe Collection
 ↓

ConcurrentHashMap
 

사실상 지금까지 배운 동시성 기술이 전부 들어가 있다.


46. 핵심 흐름 요약

put()

↓

Bucket 비어 있음

↓

CAS

----------------

Bucket 사용 중

↓

synchronized

----------------

get()

↓

volatile 읽기

↓

Lock 없음
 

47. 가장 중요한 핵심 한 줄

ConcurrentHashMap은 JDK 8 기준으로 CAS와 Bucket 단위 synchronized를 조합하여 Thread-Safe를 보장하면서도 높은 동시성을 제공하는 고성능 Map 구현체이다.
 

48. 다음 글 예고

다음 글은:

병렬 Stream(parallelStream) 주의점

이다.

많은 개발자가:

 
stream()
 

 
parallelStream()
 

으로 바꾸면 무조건 빨라질 거라고 생각하지만,

실제로는 성능이 더 나빠지는 경우가 많다.

여기서:

  • ForkJoinPool
  • CPU Bound
  • I/O Bound
  • Thread Safety
  • 순서 보장 문제

까지 모두 연결된다.

반응형

댓글