HomeBlogGuestbookLab 

JDM's Blog

온갖 테스트 결과가 기록되는 이곳은 JDM's Blog입니다. :3

Hashtable, HashMap, ConcurrentHashMap 비교

이번엔 Map 인터페이스를 구현한 콜렉션 객체들 중에서도 많이 사용되는 것으로 추정(?)되는 것들을 비교 분석하는 포스트가 되겠습니다.

Hashtable, HashMap, ConcurrentHashMap

위에 나열된 클래스들은 Map 인터페이스를 구현한 콜렉션들입니다. 이 콜렉션들은 비슷한 역할을 하는것 같으면서도 다르게 구현되어 있습니다. 기본적으로 Map 인터페이스를 구축한다면 <key, value>구조를 가지게 됩니다. 하나씩 살펴봅시다.

Hashtable

Hashtableput, get과 같은 주요 메소드에 synchronized 키워드가 선언 되어 있습니다. 또한 key, value에 null을 허용하지 않습니다. 아래의 코드는 기본적인 사용법입니다.

package Post197;

import java.util.Hashtable;

public class HashTableBasic {
	public static void main(String[] args) {
		Hashtable<String, Integer> ht = new Hashtable<>();

		ht.put("key", 0);

		/*
		 * Hashtable은 값에 null을 허용하지 않는다.
		 */
		try{
			ht.put("key2", null); // error!
		} catch( Exception e ){
			e.printStackTrace();
		}

		/*
		 * Hashtable은 키값에 null을 허용하지 않는다.
		 */
		try{
			ht.put(null, 0); // error!
		} catch( Exception e ){
			e.printStackTrace();
		}

		// 해당 키 값을 가져온다.
		ht.get("key"); // 0
	}
}

HashMap

HashMap은 주요 메소드에 synchronized 키워드가 없습니다. 또한 Hashtable과 다르게 key, value에 null을 입력할 수 있습니다.

package Post197;

import java.util.HashMap;
import java.util.Map.Entry;

public class HashMapBasic {
	public static void main(String[] args) {
		HashMap<String, Integer> hm = new HashMap<>();

		// 값 입력
		hm.put("key", 0);

		// HashMap은 값에 null을 허용한다.
		hm.put("key2", null);

		/*
		 * HashMap은 키값에 null을 허용한다.
		 */
		hm.put(null, 0);

		// 값 출력
		hm.get("key");

		// 반복 처리 with keySet
		for( String s : hm.keySet() ){
			System.out.println(hm.get(s));
		}

		// 반복 처리 with entrySet
		for( Entry<String, Integer> s : hm.entrySet() ){
			System.out.println(s.getKey()+" "+s.getValue());
		}
	}
}

ConcurrentHashMap

HashMapthread-safe 하도록 만든 클래스가 ConcurrentHashMap입니다. 하지만 HashMap과는 다르게 key, value에 null을 허용하지 않습니다. 또한 putIfAbsent라는 메소드를 가지고 있습니다. 해당 부분은 아래 코드의 주석을 참고하시기 바랍니다.

package Post197;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapBasic {

	public static void main(String[] args) {

		ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();

		// 값 입력
		chm.put("key", 0);

		/*
		 * ConcurrentHashMap은 값에 null을 허용하지 않는다.
		 */
		try{
			chm.put("key1", null); // error!
		} catch( Exception e ){
			e.printStackTrace();
		}

		/*
		 * ConcurrentHashMap은 키값에 null을 허용하지 않는다.
		 */
		try{
			chm.put(null, 0); // error!
		} catch( Exception e ){
			e.printStackTrace();
		}

		/*
		 * putIfAbsent 메소드는 키값이 존재하면 기존의 값을 반환하고
		 * 없다면 입력한 값을 저장한 뒤 반환한다.
		 * 따라서 아래의 코드는 이미 key라는 값에 0이라는 값이 있으므로
		 * key 값은 0을 반환한다.
		 */
		chm.putIfAbsent("key", 1);

		/*
		 * 아래 코드는 key2의 값이 없기 때문에 -1을 저장하고 반환한다.
		 */
		chm.putIfAbsent("key2", -1);

		for( String s : chm.keySet() ){
			System.out.println(chm.get(s)); // print -1, 0
		}
	}
}

Common Methods

위의 세종류의 클래스들은 put, get 메소드 외에도 기본적인 메소드들을 구현하고 있습니다. 대표적인 몇가지의 메소드들만 알아봅시다.

  • clear()
    해당 콜렉션의 데이터를 초기화 합니다.
  • containsKey(key)
    해당 콜렉션에 입력 받은 key를 가지고 있는지 체크합니다.
  • containsValue(value)
    해당 콜렉션에 입력 받은 value를 가지고 있는지 체크합니다.
  • remove(key)
    해당 콜렉션에 입력 받은 key의 데이터(key도 포함)를 제거합니다.
  • isEmpty()
    해당 콜렉션이 비어 있는지 체크합니다.
  • size()
    해당 콜렉션의 엔트리(Entry) 또는 세그먼트(Segment) 사이즈를 반환합니다.

In Multi Threads

사실, 이 포스트에서 가장 쓰고 싶었던 부분입니다. 위에서 살펴본 기본적인 사항에 의해 각각의 콜렉션들이 조금씩 다르다는 것을 알았습니다. 그리고 그중에서 제가 주의깊게 본 사항은 동기화 부분입니다. 실제로 동기화를 위해선 어떤 콜렉션을 써야하는지, 왜 그래야 하는지에 대한 코드 테스트를 진행했습니다.

아래의 코드는 위에서 서술한 콜렉션들을 10개의 스레드에서 각각 1000번을 반복하며 랜덤한 값을 입력할 때 엔트리의 사이즈를 비교하는 코드입니다. (성능 테스트는 아닙니다.)

코드 분석이 귀찮으시다면 아래로 그냥 내리셔도 됩니다! :3

package Post197;

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

위의 콘솔 결과를 보듯이 HashMap에 대한 부분은 동기화가 이루어지지 않아 엔트리 사이즈가 비정상적으로 나오는 것을 알 수 있습니다. 하지만 HashMap을 쓰더라도 synchronized 블록을 선언해 주면 정상으로 동작을 합니다. 따라서 동기화 이슈가 있다면 일반적인 HashMap을 쓰지 말거나 쓰더라도 동기화를 보장하는 HashMap 콜렉션 또는 synchronized 키워드를 이용해 동기화 처리를 반드시 해주는 것이 좋아보입니다.

Closing Remarks

간단하게 Map 인터페이스를 구현한 콜렉션들의 비교 및 분석을 해봤습니다. 혹시라도 잘못된 점이 있으면 댓글 부탁드립니다. :D