언어/Java
ConcurrentHashMap
린예라
2024. 11. 23. 00:24
Concurrent의 영어 뜻은 돌시발생의, 일치하는, 경쟁상대 등이 있다.
즉 동시성을 지닌 해쉬맵이라고 보면 된다.
여러 스레드에서 공유해도 안전성을 보장한다는 것이다. 멀티스레드 환경에서 동시성과 성능을 모두 고려하여 설계되었다.
특징
1) 멀티스레드 안전 (Thread-Safe)
- ConcurrentHashMap은 멀티스레드 환경에서 동기화 문제 없이 안전하게 사용할 수 있습니다.
- 내부적으로 동기화를 통해 데이터를 보호하며, 읽기/쓰기 작업을 효율적으로 처리합니다.
2) 높은 성능
- synchronized를 사용하여 전체 맵을 잠그는 대신, 세분화된 잠금(Lock Striping)을 사용합니다.
- 자바 8 이전에는 세그먼트(Segment)라는 구조로 나누어 락을 관리했으며, 자바 8부터는 CAS(Compare-And-Swap)와 bucket-level 동기화로 구현되어 더 간단하고 효율적입니다.
3) Null 키와 Null 값 금지
- ConcurrentHashMap은 null 키나 null 값을 허용하지 않습니다.
- 이는 HashMap과의 주요 차이점 중 하나입니다.
- 이유: null 값은 멀티스레드 환경에서 의미를 모호하게 만들 수 있기 때문입니다.
비교
HashMap vs ConcurrentHashMap vs Hashtable
특징 | HashMap | ConcurrentHashMap | Hashtable |
스레드 안전성 | 스레드 안전하지 않음 | 스레드 안전 | 스레드 안전 |
동기화 방식 | 없음 | 부분 동기화 (세분화된 락) | 전체 동기화 (synchronized) |
Null 키/값 | 1개의 null 키와 여러 개의 null 값 허용 | null 키/값 허용하지 않음 | null 키/값 허용하지 않음 |
성능 | 단일 스레드 환경에서 가장 빠름 | 멀티스레드 환경에서 높은 성능 | 멀티스레드 환경에서 낮은 성능 |
싱글스레드에서는 HashMap이 ConcurrentHashMap에 비해 빠르고 간단하니 더 적합하다.
멀티스레드 환경에서는 안정성과 최대한의 성능을 챙긴 ConcurrentHashMap를 사용하면 된다.
Hashtable은 레거시 코드로 , ConcurrentHashMap의 등장 이후 사용하지 않는 것을 권장한다.
즉 기존의 Hashtable은 전체에 동기화를 걸었다면, 이것의 성능을 최대한 올리기 위해 부분적으로 동기화를 적용한 상위호환이 ConcurrentHashMap 이라고 볼 수 있다.
ConcurrentHashMap의 간단 동작
자바 8 이전: 세그먼트(Segment) 기반
- ConcurrentHashMap은 내부적으로 세그먼트라는 구조로 나뉘어, 각 세그먼트에 대해 별도의 락을 걸어 동기화했습니다.
- 장점: 동시성이 높은 경우, 다른 스레드가 다른 세그먼트를 수정할 수 있음.
자바 8 이후: CAS와 버킷 락
- 자바 8부터는 세그먼트 구조를 제거하고, 더 간단하고 효율적인 방식으로 변경되었습니다.
- CAS(Compare-And-Swap): 값을 원자적으로 갱신하기 위해 사용.
- 버킷 락: 특정 버킷에만 동기화 적용.
단점
- 단일 스레드 환경에서는 HashMap보다 느림.
- null 키와 값을 사용할 수 없어 일부 코드를 수정해야 할 수 있음.
- 아주 높은 동시성에서 락 경합이 발생할 가능성.
사용예제
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 여러 스레드에서 동시 접근
Thread t1 = new Thread(() -> map.put("A", 1));
Thread t2 = new Thread(() -> map.put("B", 2));
Thread t3 = new Thread(() -> {
// computeIfAbsent: 키 "C"가 존재하지 않으면 값을 계산하여 추가.
// 여기서 키 "C"가 없으므로 람다식 key -> 3이 실행되고, "C"에 값 3이 추가됩니다.
map.computeIfAbsent("C", key -> 3);
});
// 스레드 시작
t1.start();
t2.start();
t3.start();
// 스레드 완료 대기
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
map.forEach((key, value) -> System.out.println(key + ": " + value));
/* 최종 결과 출력
A: 1
B: 2
C: 3
*/
}
}
만약 위 코드를 일반 HashMap에서 실행하면 정상적으로 결과가 출력될 수도 있지만, 동시에 접근하여 값이 덮어써져서 누락되거나 문제가 생길 수 있다.
위그나마 3개의 스레드라 그나마 오류나 문제가 생길 확률이 적을 수 있지만, 스레드의 숫자가 늘어날 수록 문제가 생길확률도 크게 올라갈 것이다.