이 번에는 안전한 멀티 스레드 환경을 만들기 위해 Java에서 제공하는 ConcurrentHashMap에 대해 학습한 내용을 정리해보겠습니다.
ConcurrentHashMap은 Java에서 동시성 기능을 제공하는 Map 자료구조입니다. 먼저 간단하게 테스트를 진행한 후에 구현부에 대해서 정리해보겠습니다.
테스트 코드는 다음과 같습니다.
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class concurrent {
public static ConcurrentHashMap<Long, Integer> concurrentHashMap = new ConcurrentHashMap<>();
public static HashMap<Long, Integer> hashMap = new HashMap<>();
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < 5 ; i++){
executorService.submit(() -> hashMap.put(1L, hashMap.getOrDefault(1L, 0) + 1));
}
for(int i = 0 ; i < 5 ; i++){
executorService.submit(() -> concurrentHashMap.put(1L, concurrentHashMap.getOrDefault(1L, 0) + 1));
}
Thread.sleep(1000);
System.out.println("hashMap : " + hashMap.get(1L));
System.out.println("concurrentHashMap : " + concurrentHashMap.get(1L));
}
}
해당 테스트 코드를 실행하게 되면 다음과 같은 결과를 얻을 수 있는데 여기서 확인할 수 있는 내용은 HashMap의 경우에는 멀티 쓰레드 환경에서 적합하지 않다는 것을 알 수 있습니다.
(기대값이 5이지만 HashMap의 결과는 4)
그렇다면 HashMap과 ConcurrentHashMap의 구현 차이를 정리해보겠습니다.
volatile V val;
volatile Node<K,V> next;
가장 먼저 ConcurrentHashMap의 val과 next 앞에 volatile 키워드가 붙어있습니다. 해당 키워드는 데이터를 메모리에 저장하고 메모리에서 읽어오도록 하는 설정입니다.
실제 프로그램에서는 데이터를 빨리 읽어오기 위해 데이터를 읽어온 데이터를 캐시에 저장하고 사용합니다. 하지만 캐시의 경우에는 데이터의 정합성이 맞지 않는 경우가 생길 수 있는데 이러한 점을 예방하기 위해 volatile 키워드를 사용했다고 생각합니다.
내용이 길지만 키포인트는 Synchronized입니다. Synchronized는 여러개의 스레드가 한개의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념입니다. 이를 통해 동시에 많은 스레드가 ConcurrentHashMap의 값을 변경하려고 접근을 해도 데이터를 지킬 수 있게 됩니다.
처음에는 메소드 전체에 Synchronized를 설정하지 않을까 했지만 정말 필요한 코드에만 Synchronized를 설정하여 성능적인 면을 생각한 것이 눈에 띄었습니다.
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; K fk; V fv;
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)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
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);
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
참고 문서 및 링크