자바에서 키-값(Key-Value) 기반의 데이터를 저장할 때 가장 많이 사용되는 자료구조 중 하나가 해시 기반 컬렉션(Hash-based Collection) 입니다. 대표적으로 HashMap
, Hashtable
, 그리고 ConcurrentHashMap
이 있으며, 각각의 특징과 차이점을 이해하는 것이 중요합니다. 이번 글에서는 이 세 가지 클래스의 차이점과 어떤 상황에서 어떤 클래스를 사용해야 하는지 정리해 보겠습니다.
HashMap
은 멀티 스레드 환경에서 안전하지 않음.null
키 1개, null
값은 여러 개 저장 가능.HashMap
은 데이터를 저장하기 위해 배열 + 연결 리스트 + 트리(Red-Black Tree, JDK 8 이후) 구조를 사용한다.static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
private V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((tab[i = (n - 1) & hash]) == null)
tab[i] = new Node<>(hash, key, value, null);
else {
Node<K,V> p = tab[i];
while (p.next != null) {
p = p.next;
}
p.next = new Node<>(hash, key, value, null);
}
return null;
}
synchronized
키워드로 보호되어 멀티 스레드 환경에서 안전함.null
키와 null
값을 허용하지 않음.HashMap
보다 느림.Hashtable
은 HashMap
과 유사한 구조를 가지고 있지만, 모든 메서드에 synchronized
가 적용되어 있음.private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
public synchronized V put(K key, V value) {
if (value == null) throw new NullPointerException();
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;
for (Entry<K,V> e = table[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
table[index] = new Entry<>(hash, key, value, table[index]);
return null;
}
synchronized
블록으로 인해 하나의 스레드만 접근 가능 → 성능 병목 발생 가능.HashMap
.null
키와 null
값을 허용하지 않음.static class Node<K,V> {
final int hash;
final K key;
volatile V value;
volatile Node<K,V> next;
}
public V put(K key, V value) {
return putVal(key, value, false);
}
private final V putVal(K key, V value, boolean onlyIfAbsent) {
int hash = spread(key.hashCode());
Node<K,V>[] tab; Node<K,V> f; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
tab = resize();
if ((f = tab[i = (n - 1) & hash]) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
return null;
}
synchronized (f) {
// 연결 리스트 또는 트리 탐색 후 값을 갱신
}
return null;
}
Hashtable
보다 훨씬 빠름.get()
같은 읽기 연산은 락 없이 수행 가능.특징 | HashMap | Hashtable | ConcurrentHashMap |
---|---|---|---|
동기화 여부 | X | O | O (세분화된 락 적용) |
멀티 스레드 환경 | X | O (성능 저하) | O (성능 우수) |
null 허용 여부 | O | X | X |
성능 | 빠름 | 느림 | 빠름 |
락 방식 | 없음 | 전체 테이블 락 | 버킷 단위 락 (JDK 8 이후) |
멀티 스레드 환경에서는 ConcurrentHashMap
을 사용해야 하며, Hashtable
은 더 이상 권장되지 않는다. 단일 스레드 환경에서는 HashMap
이 가장 적합하다.