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
| 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
- 순서 보장 문제
까지 모두 연결된다.
'language > java' 카테고리의 다른 글
| Virtual Thread (Java 21) 완벽 이해하기 (0) | 2026.06.01 |
|---|---|
| 병렬 Stream(parallelStream) 주의점 완벽 이해하기 (0) | 2026.06.01 |
| DeadLock(교착상태) 원인과 해결 완벽 이해하기 (0) | 2026.06.01 |
| ReentrantLock 완벽 이해하기 (0) | 2026.06.01 |
| ForkJoinPool 완벽 이해하기 (0) | 2026.06.01 |
댓글