2014년 6월 2일 월요일

Java ConcurrentHashMap

Java ConcurrentHashMap

배경

ConcurrentHashMap은 Java 1.5 버전에서 HashTable의 대안으로 소개 되었다. Java 1.5 버전 이전에는 concurrent하고 multi-threaded 를 고려한 map을 구현하려면 HashTable 또는 synchronized map을 사용해야 했다. 왜냐하면 HashMap은 thread-safe 하지 않았기 때문이다.

ConcurrentHashMap은 concurrent multi-threaded 환경에서 안정적으로 동작하고 HashTable과 synchronized map 보다 더 나은 성능을 가지고 있다. 그 이유는, ConcurrentHashMap은 map의 일부에만 lock을 거는데 반해, 앞의 두 가지는 map 전체에 lock을 걸기 때문이다.

동작원리

Concurrency 레벨에 기반하여 map을 여러 파트로 나누어 두고, 업데이트 동작이 일어나는 동안 오직 그 부분에만 lock을 수행한다. 기본 Concurrency 레벨은 16이다. Map을 16개의 부분으로 나누고 각각의 파트에는 서로 다른 lock을 수행한다. 다시 말하면, 16개의 쓰레드가 동시다발적으로 map에 동작을 수행할 수 있게 되는 것이다. 하지만, put(), remove(), putAll(), clear() 등의 명령어는 동기적으로 수행되기 때문에, map의 최신결과를 반영하지 않을 수도 있다.

KeySet iterator의 경우에는 주기적으로 동기화 되고 특정한 시점의 상태만을 반영하기 때문에 최근에 변경된 내용은 반영하지 못한다. ConcurrentHashMap의 iterator는 fail-safe하고 ConcurrentModificationException을 발생시키지 않는다.

어떨 때 사용하는가?

읽기가 쓰기보다 많을 때 가장 적합하다. 만약 쓰기가 더 많거나 쓰기가 읽기와 비슷하다면 ConcurrentHashMap의 성능은 synchronized map 또는 HashTable 만큼 떨어지게 된다. ConcurrentHashMap은 어플리케이션의 구동 후에 많은 요청을 처리하는 쓰레드에서 엑세스 하는 동안 초기화할 수 있기 때문에 cache 용도로 아주 적합하다. ConcurrentHashMap은 또한 HashTable의 좋은 대체안이기도 하며 가능한한 ConcurrentHashMap을 사용하라고 javadoc에 기술되어 있다. 다만 ConcurrentHashMap이 동기화 부분에는 HashTable 보다 조금 단점을 가지고 있다고 보면 되겠다.

HashTable과의 차이점

ConcurrentHashMap과 HashTable 모두 멀티쓰레드 환경에서 사용될 수 있으나, HashTable은 크기가 매우 커지면 iteration을 하기 위해 lock을 오래동안 걸어야 하므로 성능이 크게 저하된다.

putIfAbsent()

if (map.get(key) == null){
    return map.put(key, value);
} else{
    return map.get(key);
}

위와 같은 소스는 ConcurrentHashMap일 경우 원하는 대로 동작하지 않는다. put 명령을 수행할 때 map 전체를 잠그지 않기 때문이다. A쓰레드가 put 을 수행하는 중에 B쓰레드가 get() 명령을 호출하면 null 값을 리턴 받게 된다. 물론 코드 전체를 synchronized block으로 감싸서 thread-safe 하게 만들 수는 있지만 코드는 싱글 쓰레드에서만 돌아가므로 ConcurrentHashMap의 이점이 사라지게 된다. 그래서 ConcurrentHashMap에서는 putIfAbsent(key,value) 형식의 함수를 제공하여 race condition을 제거하고 동일한 동작을 수행한다.