본문 바로가기
Programing/Java

HashMap, Hashtable, ConcurrentHashMap 동기화 처리 방식

by Tomining 2016. 12. 20.
먼저 HashMap과 HashTable의 차이에 대해서 알아보자.
Key, Value 형식의 데이터를 저장하는 기능을 제공하는 자료 구조로 큰 맥락에서는 동일하다. 그러나 크게 3가지 정도의 차이를 보인다.
  • Null 허용 여부
  • 동기화 여부
  • iterator/Enumeration

HashMap은 Key나 Value에 Null 값을 허용하지만 HashTable이나 ConcurrentHashMap은 Null을 허용하지 않는다.
그리고 내부 아이템을 순회하기 위한 기능을 제공하는데, 그 부분에 대해서도 차이가 있다.(자세한 내용은 아래 링크 참조)

여기서는 HashMap, HashTable, ConcurrentHashMap에서 동기화 처리가 어떻게 다른지에 대해서 알아보도록 하자.

HashMap
HashMap은 Thread-Safe하지 못한 클래스이다. 따라서 MultiThread 환경에서 사용하려면 꼭 동기화처리가 필요하다.
Collections.synchronizedMap(hashMap) 등을 이용하여 동기화 처리 후 사용하자.

Hashtable
get(), put() 메서드를 확인해보면 synchronized하게 구현되어 있음을 확인할 수 있다.
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}
즉, Hashtable은 동기화 처리가 되어 있기 때문에 MultiThread 환경에서도 안전하게 사용할 수 있다.

ConcurrentHashMap
ConcurrentHashMap은 Hashtable과는 조금 차이가 있다. 먼저 putVal() 메서드 구현 부분을 확인해 보자.(아래 코드 참조)
기본적으로 put() 메서드에 synchronized 키워드가 없다. 아래 코드를 보면 동일 Entry(아래 코드에서는 Node)에 추가 될 경우 해당 Entry에 대해서 synchronized를 적용한 것을 확인할 수 있다. 즉, Entry 배열 아이템 별로 동기화 처리를 하고 있는 것이다. 이런 방식은 MultiThread 환경에서 성능을 크게 향상시킨다. 
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

MultiThread 환경 테스트
아래 코드는 http://jdm.kr/blog/197 에서 가져왔다. MultiThread 환경에서 각 자료구조에 put() 메서드를 실행해 보는 테스트 케이스이다. 종료 후 결과를 보면 HashMap만 Entry 사이즈가 다른 것을 확인 할 수 있다.
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MultiThreadsTest {

        private static final int MAX_THREADS = 10;

        private static Hashtable<String, Integer> ht = new Hashtable<>();
        private static HashMap<String, Integer> hm = new HashMap<>();
        private static HashMap<String, Integer> hmSyn = new HashMap<>();
        private static Map<String, Integer> hmSyn2 = Collections.synchronizedMap(new HashMap<String, Integer>());
        private static ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();

        public static void main(String[] args) throws InterruptedException {

                ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);

                for( int j = 0 ; j < MAX_THREADS; j++ ){
                        es.execute(new Runnable() {
                                @Override
                                public void run() {
                                        for( int i = 0; i < 1000; i++ ){

                                                String key = String.valueOf(i);

                                                ht.put(key, i);
                                                hm.put(key, i);
                                                chm.put(key, i);
                                                hmSyn2.put(key, i);

                                                synchronized (hmSyn) {
                                                        hmSyn.put(key, i);
                                                }
                                        }
                                }
                        });
                }

                es.shutdown();
                try {
                        es.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }

                System.out.println("Hashtable size is "+ ht.size());
                System.out.println("HashMap size is "+ hm.size());
                System.out.println("ConcurrentHashMap size is "+ chm.size());
                System.out.println("HashMap(synchronized) size is "+ hmSyn.size());
                System.out.println("synchronizedMap size is "+ hmSyn2.size());

                /*
                for( String s : hm.keySet() ){
                        System.out.println("["+s+"] " + hm.get(s));
                }
                */
        }
}
Hashtable size is 1000
HashMap size is 1281
ConcurrentHashMap size is 1000
HashMap(synchronized) size is 1000
synchronizedMap size is 1000



결론
MultiThread 환경에서 동기화 처리가 필요하다면 ConcurrentHashMap을 사용하는 것이 안정적이다. 동기화가 필요하지 않은 경우라면 그냥 HashMap을 사용하자. Hashtable은 Thread-Safe 하긴 하나 느리다는 단점이 있다.


참고