[Java] HashMap vs HashTable vs ConcurrentHashMap

오형상·2025년 2월 27일
0

java

목록 보기
9/9
post-thumbnail

자바에서 키-값(Key-Value) 기반의 데이터를 저장할 때 가장 많이 사용되는 자료구조 중 하나가 해시 기반 컬렉션(Hash-based Collection) 입니다. 대표적으로 HashMap, Hashtable, 그리고 ConcurrentHashMap이 있으며, 각각의 특징과 차이점을 이해하는 것이 중요합니다. 이번 글에서는 이 세 가지 클래스의 차이점과 어떤 상황에서 어떤 클래스를 사용해야 하는지 정리해 보겠습니다.


1. HashMap

특징

  • 비동기(Non-Synchronized): HashMap은 멀티 스레드 환경에서 안전하지 않음.
  • null 허용: null 키 1개, null 값은 여러 개 저장 가능.
  • 순서 보장 없음: 내부적으로 해싱(Hashing)을 사용하므로 삽입 순서를 유지하지 않음.
  • 성능 우수: 동기화가 없기 때문에 싱글 스레드 환경에서 가장 빠른 성능을 제공.

내부 구조

  • 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;
    }
}

put() 메서드 구현

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;
}

단점

  • 멀티 스레드 환경에서 동기화가 보장되지 않음 → 동시 접근 시 데이터 손실 발생 가능.
  • 해시 충돌이 많아지면 성능 저하 → JDK 8부터 연결 리스트를 Red-Black Tree 로 변환하여 해결.

2. Hashtable

특징

  • 동기화(Synchronized) 지원: 모든 메서드가 synchronized 키워드로 보호되어 멀티 스레드 환경에서 안전함.
  • null 허용 X: null 키와 null 값을 허용하지 않음.
  • 성능 저하: 동기화로 인해 성능이 HashMap보다 느림.

내부 구조

  • HashtableHashMap과 유사한 구조를 가지고 있지만, 모든 메서드에 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;
    }
}

put() 메서드 구현

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 블록으로 인해 하나의 스레드만 접근 가능 → 성능 병목 발생 가능.
  • 조회 연산에도 동기화 적용 → 불필요한 락이 발생하여 성능 저하.

3. ConcurrentHashMap

특징

  • 고성능 동시성 지원: 멀티 스레드 환경에서도 높은 성능을 제공하는 비블로킹(Non-Blocking) 방식의 HashMap.
  • null 허용 X: null 키와 null 값을 허용하지 않음.
  • JDK 8 이후 락 방식 변경:
    • JDK 7까지는 세그먼트 락(Segment Locking)
    • JDK 8부터 버킷 락(Bucket Locking) + CAS(Compare-And-Swap) 사용하여 성능 최적화.

내부 구조

static class Node<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile Node<K,V> next;
}

put() 메서드 구현 (JDK 8 이후)

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보다 훨씬 빠름.
  • CAS(Compare-And-Swap) 기반의 동기화 → 불필요한 락을 줄여 동시성 향상.
  • get() 같은 읽기 연산은 락 없이 수행 가능.

4. HashMap vs Hashtable vs ConcurrentHashMap 비교

특징HashMapHashtableConcurrentHashMap
동기화 여부XOO (세분화된 락 적용)
멀티 스레드 환경XO (성능 저하)O (성능 우수)
null 허용 여부OXX
성능빠름느림빠름
락 방식없음전체 테이블 락버킷 단위 락 (JDK 8 이후)

멀티 스레드 환경에서는 ConcurrentHashMap을 사용해야 하며, Hashtable은 더 이상 권장되지 않는다. 단일 스레드 환경에서는 HashMap이 가장 적합하다.

0개의 댓글