ConcurrentHashMap — 동시성 해시 맵
ConcurrentHashMap — 동시성 해시 맵
ConcurrentHashMap<K,V>는 조회(retrieval)의 완전한 동시성과 업데이트의 높은 예상 동시성을 지원하는 해시 테이블이에요. 여러 스레드가 같은 맵을 동시에 읽고 쓰는 환경에서 Hashtable을 대체하거나, HashMap에 동기화를 덧붙이는 대신 쓰는 표준 선택이에요.
본문
개념 이해하기
ConcurrentHashMap은 Hashtable과 같은 기능적 명세를 따르고 각 메서드에 대응하는 버전을 포함해요. 모든 연산이 스레드 안전하지만, 조회 연산은 락을 걸지 않고, 전체 테이블을 잠가 모든 접근을 막는 방식을 지원하지 않아요. 스레드 안전성에 의존하되 동기화 세부 사항엔 의존하지 않는 프로그램에서 Hashtable과 완전히 상호운용돼요.
public class ConcurrentHashMap<K,V>
extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable
주요 동작 특성
- 조회 연산(
get포함)은 일반적으로 블로킹하지 않아요, 그래서 업데이트 연산(put,remove등)과 겹칠 수 있어요. 조회는 시작 시점에 가장 최근에 완료된 업데이트의 결과를 반영해요. 특정 키에 대한 업데이트는 그 키의 (null 아님) 조회와 happen-before 관계예요. - 일괄/집계 연산(
putAll,clear)은 동시 조회가 일부 엔트리의 삽입·제거만 반영할 수 있어요. Iterators, Spliterators, Enumerations는 생성 시점 이후 어느 시점의 해시 테이블 상태를 반영하며ConcurrentModificationException을 던지지 않아요. 다만 iterator는 한 번에 한 스레드만 쓸 수 있게 설계됐어요. size,isEmpty,containsValue같은 집계 상태 메서드의 결과는 맵이 다른 스레드에서 동시 업데이트되지 않을 때만 보통 유용해요. 그렇지 않으면 모니터링·추정용으로는 충분하지만 프로그램 제어용으론 부적합한 일시적 상태를 반영할 수 있어요.- 동적 확장: 충돌(collision)이 너무 많으면 테이블을 동적으로 확장해 매핑당 대략 2개의 bin을 유지해요(0.75 로드 팩터 임계값에 대응). 리사이징은 상대적으로 느릴 수 있으므로 가능하면
initialCapacity생성자 인자로 크기 추정치를 주는 게 좋아요. Hashtable처럼null을 키나 값으로 허용하지 않아요(HashMap과 다른 점).newKeySet()/newKeySet(int)로 Set 투영을 만들거나keySet(Object)로 볼 수 있어요.
확장 가능한 빈도 맵 활용
LongAdder 값을 쓰고 computeIfAbsent로 초기화하면 확장 가능한 빈도 맵을 만들 수 있어요:
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
생성자
public ConcurrentHashMap() — 기본 초기 테이블 크기(16)의 새 빈 맵을 만들어요.
public ConcurrentHashMap(int initialCapacity) — 동적 리사이즈 없이 지정된 수의 요소를 수용하는 초기 테이블 크기의 새 빈 맵을 만들어요.
IllegalArgumentException— 초기 용량이 음수일 때
public ConcurrentHashMap(Map<? extends K,? extends V> m) — 주어진 맵과 같은 매핑을 가진 새 맵을 만들어요.
public ConcurrentHashMap(int initialCapacity, float loadFactor) — 주어진 요소 수와 초기 테이블 밀도(로드 팩터)에 기반한 초기 테이블 크기의 새 빈 맵을 만들어요. (JDK 1.6+)
IllegalArgumentException— 용량이 음수이거나 로드 팩터가 양수가 아닐 때
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) — 요소 수, 테이블 밀도, 동시 업데이트 스레드 수를 모두 고려한 초기 테이블 크기의 새 빈 맵을 만들어요.
IllegalArgumentException— 음수 용량 또는 0 이하의 로드 팩터/동시성 수준
기본 맵 연산
public int size() — 키-값 매핑 수를 반환해요. Integer.MAX_VALUE를 넘으면 Integer.MAX_VALUE를 반환해요.
public boolean isEmpty() — 매핑이 없으면 true.
public V get(Object key) — 지정 키에 매핑된 값을 반환하거나, 매핑이 없으면 null.
NullPointerException— 키가null일 때
public boolean containsKey(Object key) — 키가 있으면 true.
NullPointerException
public boolean containsValue(Object value) — 하나 이상의 키가 지정 값에 매핑되면 true. 맵 전체 순회가 필요해 containsKey보다 훨씬 느려요.
NullPointerException
public V put(K key, V value) — 키와 값을 매핑해요. 키나 값 모두 null이 될 수 없어요. 이전 값 또는 null을 반환해요.
NullPointerException
public void putAll(Map<? extends K,? extends V> m) — 모든 매핑을 복사해요.
public V remove(Object key) — 키(와 그 값)를 제거해요. 이전 값 또는 null을 반환해요.
NullPointerException
public void clear() — 모든 매핑을 제거해요.
public boolean contains(Object value) — 어떤 키가 값에 매핑되는지 검사해요. containsValue(Object)와 기능적으로 동일하며, Java Collections Framework 도입 전에 이 메서드를 지원했던 Hashtable과의 완전한 호환을 위해 존재해요.
NullPointerException
public int hashCode() — 맵의 각 키-값 쌍에 대해 key.hashCode() ^ value.hashCode()의 합을 반환해요.
public String toString() — 맵의 문자열 표현(중괄호 안의 키-값 매핑 목록)을 반환해요.
public boolean equals(Object o) — 같은 매핑을 가진 맵인지 비교해요. 실행 중 어느 한쪽이 동시 수정되면 오해의 소지가 있는 결과를 줄 수 있어요.
public long mappingCount() — 매핑 수를 반환해요. int로 표현할 수 있는 것보다 많은 매핑을 담을 수 있어 size() 대신 이 메서드를 써야 해요. 값은 추정치이며 동시 삽입·제거가 있으면 실제 수와 다를 수 있어요. (JDK 1.8+)
뷰 메서드
public ConcurrentHashMap.KeySetView<K,V> keySet() — 키의 Set 뷰를 반환해요. 맵에 의해 백업되며, 요소 제거(Iterator.remove, Set.remove, removeAll, retainAll, clear)를 지원하지만 add/addAll은 지원하지 않아요. iterator·spliterator는 약하게 일관적(weakly consistent)이며, spliterator는 CONCURRENT, DISTINCT, NONNULL을 보고해요.
public Collection<V> values() — 값의 Collection 뷰를 반환해요. 요소 제거를 지원하되 add/addAll은 지원하지 않아요. spliterator는 CONCURRENT, NONNULL.
public Set<Map.Entry<K,V>> entrySet() — 매핑의 Set 뷰를 반환해요. spliterator는 CONCURRENT, DISTINCT, NONNULL.
public Enumeration<K> keys() — 키의 열거형을 반환해요.
public Enumeration<V> elements() — 값의 열거형을 반환해요.
KeySet 팩토리
public static <K> ConcurrentHashMap.KeySetView<K,Boolean> newKeySet() — 주어진 타입에서 Boolean.TRUE로 매핑되는 ConcurrentHashMap에 백업된 새 Set을 만들어요. (JDK 1.8+)
public static <K> ConcurrentHashMap.KeySetView<K,Boolean> newKeySet(int initialCapacity) — 초기 용량을 지정해 새 Set을 만들어요. (JDK 1.8+)
IllegalArgumentException— 용량이 음수일 때
public ConcurrentHashMap.KeySetView<K,V> keySet(V mappedValue) — 모든 추가(add/addAll)에 공통 매핑 값을 사용하는 키 Set 뷰를 반환해요. 모든 추가에 같은 값을 쓰는 게 적절할 때만 써요.
NullPointerException—mappedValue가null일 때
원자적 조건 연산 (ConcurrentMap)
public V putIfAbsent(K key, V value) — 키가 아직 값과 연관되지 않았다면 주어진 값과 연관시켜요. 원자적으로 수행돼요. (containsKey+put의 원자 버전)
NullPointerException
public boolean remove(Object key, Object value) — 키가 주어진 값에 매핑돼 있을 때만 해당 엔트리를 제거해요. 원자적.
NullPointerException— 키가null일 때
public boolean replace(K key, V oldValue, V newValue) — 키가 주어진 값에 매핑돼 있을 때만 새 값으로 교체해요. 원자적.
NullPointerException— 인자 중 하나가null일 때
public V replace(K key, V value) — 키가 어떤 값에 매핑돼 있을 때만 교체해요. 이전 값 또는 null을 반환해요. 원자적.
public V getOrDefault(Object key, V defaultValue) — 키 매핑 값 또는 기본 값을 반환해요.
NullPointerException— 키가null일 때
compute 계열
public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) — 키가 값과 연관되지 않았다면 매핑 함수로 값을 계산해 null이 아니면 넣어요. 전체 호출이 원자적이며, 키가 없으면 함수가 호출당 정확히 한 번 호출돼요. 계산이 진행되는 동안 다른 스레드의 업데이트가 블로킹될 수 있으므로 계산은 짧고 단순해야 해요. 계산 중 이 맵을 수정하면 안 돼요.
NullPointerException(키 또는 함수가null),IllegalStateException(감지 가능한 재귀 업데이트),RuntimeException/Error
public V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) — 키의 값이 있으면 키와 현재 값을 주고 새 매핑을 계산해요. 원자적. 함수는 키가 있을 때만 정확히 한 번 호출돼요.
public V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) — 키와 현재 값(없으면 null)으로 매핑을 계산해요. 원자적. 함수는 호출당 정확히 한 번 호출돼요.
merge
public V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) — 키가 (null 아닌) 값과 연관되지 않았다면 주어진 value와 연관시키고, 그렇지 않으면 재매핑 함수 결과로 교체하거나 null이면 제거해요. 원자적. 계산 중 다른 매핑을 업데이트하려 하면 안 돼요.
NullPointerException(키 또는 함수가null),RuntimeException/Error
병렬 일괄 연산 (bulk operations)
forEach, search, reduce 계열은 대량 연산으로, 동시에 업데이트되는 맵에도 안전하게 적용하도록 설계됐어요. 세 가지 종류가 있고, 각각 인자/반환으로 키, 값, 엔트리, (키,값) 쌍을 받아들이는 네 가지 형태가 있어요.
parallelismThreshold: 현재 맵 크기가 임계값보다 작으면 순차 진행해요.Long.MAX_VALUE는 병렬을 완전히 억제,1은 최대 병렬(ForkJoinPool.commonPool()활용).- forEach — 각 요소에 주어진 액션을 수행해요. 변환을 먼저 적용하는 변형도 있어요.
- search — 각 요소에 함수를 적용한 첫 번째 non-null 결과를 반환하고, 결과를 찾으면 추가 검색을 중단해요.
- reduce — 각 요소를 누적해요. 제공된 축소 함수는 순서에 의존할 수 없으며(결합·교환적이어야 함) 다섯 가지 변형(일반, 매핑, double/long/int 스칼라)이 있어요.
null은 "결과 없음"의 원자적 지표로 쓰여요.
주요 메서드: forEach(threshold, action), forEach(threshold, transformer, action), search(threshold, searchFunction), reduce(threshold, transformer, reducer), reduceToDouble/Long/Int(...) — 그리고 키/값/엔트리 대상의 변형: forEachKey/searchKeys/reduceKeys/reduceKeysToDouble/reduceKeysToLong/reduceKeysToInt, forEachValue/searchValues/reduceValues/reduceValuesToDouble/reduceValuesToLong/reduceValuesToInt, forEachEntry/searchEntries/reduceEntries/reduceEntriesToDouble/reduceEntriesToLong/reduceEntriesToInt. (JDK 1.8+)
주의: 병렬이 순차보다 항상 빠른 건 아니고, 작은 맵에 짧은 함수를 병렬 처리하면 순차보다 느릴 수 있어요. 모든 태스크 메서드의 인자는 non-null이어야 해요. Map.Entry 객체에 대한 일괄 연산은 setValue를 지원하지 않아요.