코딩마을방범대
[Java] Hashtable, HashMap, ConcurrentHashMap 에 대해서 본문
Hashtable
Hashtable 클래스를 살펴보면 메소드 전체에 synchronized 키워드가 적용되어 있는 것을 확인할 수 있다.
따라서 멀티쓰레드 환경에 적합하며, 쓰레드 세이프 하다는 특징이 있다.
하지만, 동시 작업을 실행하려할 경우 Lock을 하나씩 가지고 있기 때문에 동시 작업 시 병목 현상이 발생할 수 밖에 없다.
( 메소드에 접근하게 되면 다른 쓰레드는 Lock을 얻을 때까지 기다림 )
※ Collection Framework가 나오기 이전부터 존재하는 클래스이기 때문에 최근에는 잘 사용하지 않는 클래스라고 한다.
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
public synchronized int size() { }
@SuppressWarnings("unchecked")
public synchronized V get(Object key) { }
public synchronized V put(K key, V value) { }
}
Synchronized Keyword
여러개의 스레드가 한 개의 자원에 접근할려고 할 때,
현재 데이터를 사용하고 있는 스레드를 제외하고 나머지 스레드들이 데이터에 접근할 수 없도록 막는 역할을 수행
HashMap
HashMap 클래스를 살펴보면 synchronized 키워드가 적용되어 있지 않아 속도는 가장 빠르다는 장점이 있다.
하지만, 멀티쓰레드 환경에서는 부적합하며, 쓰레드 세이프 하지 않다.
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
public V get(Object key) {}
public V put(K key, V value) {}
}
ConcurrentHashMap
HashMap 클래스의 단점을 보완하면서 멀티쓰레드 환경에서 사용할 수 있도록 출시된 클래스이다.
아래는 ConcurrentHashMap 클래스의 일부분이다.
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
public V get(Object key) {}
public boolean containsKey(Object key) { }
public V put(K key, V value) {
return putVal(key, value, false);
}
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;
}
}
ConcuurentHashMap에는 Hashtable 과는 다르게 synchronized 키워드가 메소드 전체에 붙어 있지 않다.
get() 메소드에는 아예 synchronized가 존재하지 않고, put() 메소드에는 중간에 synchronized 키워드가 존재하는 것을 볼 수 있다.
이것을 좀 더 정리해보면 ConcurrentHashMap은 읽기 작업에는 여러 쓰레드가 동시에 읽을 수 있지만,
쓰기 작업에는 특정 세그먼트 or 버킷에 대한 Lock을 사용한다는 것이다.
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final int DEFAULT_CAPACITY = 16;
// 동시에 업데이트를 수행하는 쓰레드 수
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
}
ConcurrentHashMap 클래스를 보면 위와 같이 DEFAULT_CAPACITY, DEFAULT_CONCURRENCY_LEVEL가 16으로 설정되어 있다.
DEFAULT_CAPACITY | 버킷의 수 |
DEFAULT_CONCURRENCY_LEVEL | 동시에 작업 가능한 쓰레드 수 |
💡 버킷의 수와 동시 작업 가능한 쓰레드 수가 동일한 이유는?
버킷 단위로 Lock을 사용하기 때문에 같은 버킷만 아니라면 Lock을 기다릴 필요가 없다는 특징이 있다.
( 버킷당 하나의 Lock을 가지고 있다라고 생각하면 됨 )
즉, 여러 쓰레드에서 ConcurrentHashMap 객체에 동시에 데이터를 삽입, 참조하더라도
그 데이터가 다른 세그먼트에 위치하면 서로 Lock을 얻기 위해 경쟁하지 않는다.
세그먼트란?
- 데이터 베이스 시스템에서 데이터를 기억할 때의 최소 단위
- 가상 기억 장치에서 운영 체제에 의해서 어떤 바이트 수 단위로 분할되는 가상 기억 영역
효과를 극대화하기 위해서는 상황에 따라 적절히 세그먼트를 나누는 것이 필요하다.
데이터를 너무 적은 수의 조각으로 나누면 경쟁을 줄이는 효과가 적을 것이고,
너무 많은 수의 조각으로 나누면 이 세그먼트를 관리하는 비용이 커지기 때문이다.
위에서 정리한 내용으로 보아 읽기(get) 에는 동기화가 적용되지 않으므로 업데이트 작업(put, remove)과 겹칠 수 있다.
읽기(get)은 현 시점 기준 최근에 완료된 업데이트 작업의 결과를 가져온다.
ConcurrentHashMap의 메소드
newKeySet
newKeySet() 을 이용하면 ConcurrentHashMap으로 유지되는 Set으로 활용 가능하다.
Set<String> sets = ConcurrentHashMap.newKeySet();
mappingCount
mappingCount을 이용하면 size 메소드와 동일하게 해당 Map의 크기를 반환하지만,
size와 다르게 long타입을 반환하기 때문에 int 자료형의 범위를 벗어날 경우의 오류를 예방할 수 있다.
map.mappingCount()
참고사이트
[Java] ConcurrentHashMap 이란 무엇일까?
'💡 백엔드 > Java' 카테고리의 다른 글
[Java] @Transactional 에 대해서 (& 프록시객체) (0) | 2023.08.04 |
---|---|
순차 & 병렬 & 병행 처리의 차이점과 Java의 stream & parallelStream (0) | 2023.08.01 |
[Java] 컬렉션 팩토리(Set, Map, List) (0) | 2023.07.31 |
List 를 String 으로, String 을 List 로 변환하는 방법 (0) | 2023.07.28 |
[Java] String, StringBuffer, StringBuilder의 차이점 (0) | 2023.07.28 |