JAVA HashMap 과 동시성 #1 (Pub/Sub)
Note that this implementation is not synchronized.
If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
Java 의 HashMap 은 Thread Safe 하지 않기 때문에 동시에 여러 쓰레드가 접근할 경우에 외부에서 synchronized 처리가 필요하다.
멀티 쓰레드 환경에서 Thread Safe 하지 않은 HashMap 에 발생할 수 있는 현상.
final Map<Integer, String> map = new HashMap<>();
final Integer targetKey = Integer.MAX_VALUE;
final String value = "v";
map.put(targetKey, value); --- 1
Executors.newFixedThreadPool(1).execute(() -> {
IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue")); --- 2
});
while (true) {
if (map.get(targetKey) == null) --- 3
System.out.println("RESULT IS NULL"); --- 4
}
1. 새로 생성한 HashMap 에 Integer.MAX_VALUE 를 키로하고 Value 는 "v" 인 Element 를 put
2. 새로운 Thread 를 생성하여 0 부터 Integer.MAX_VALUE 까지 반복하면서 HashMap 에 put
3. (1) 에서 put 한 데이터를 get 했을때 존재하지 않으면 "RESULT IS NULL" Print
(1) 에서 분명히 HashMap 에 put 하였음에도 get() 했을때 value 가 Null 이 리턴되었다.
(4) 에 Break Point 를 잡고 HashMap 의 상태를 보았을때 Integer.MAX_VALUE 에 해당하는 Value 가 존재하고 map.get(targetKey) 를 호출했을때 정상적으로 "v" 가 리턴되었다.
그렇다면 Break Point 가 잡힐 당시에 어떤 상황이였을지 HashMap 내부 구현을 분석해보면서 유추해보자.
원인 분석
HashMap 의 put() 함수가 호출되면 HashMap 의 멤버 변수인 table(Hash Table 역할) 의 size 를 늘리는 resize() 함수가 호출된다. resize() 과정에서 this.table 에 새로운 Array 를 할당하는데 이 때 다른 method 에서 get(key) 를 호출하면 현재 table 에는 새로운 Array 가 할당되어있고 아직 AS-IS table 의 데이터를 복사해오기 전 상태이므로 데이터가 없다고 판단되어 Null Return.
에제의 HashMap 내부 HashTable 상태
1. put(4, "someV");
- HashTable 에 저장이 된다.
- HashTable 에 추가 공간이 필요하므로 resize() 호출 (정확하게는 threshold 초과시) --- (1)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
.... put 하는 과정.
if (++size > threshold)
resize(); --- (1)
afterNodeInsertion(evict);
return null;
}
2. resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; --- 1
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
...
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; --- 2
table = newTab; --- 3
if (oldTab != null) { --- 4
for (int j = 0; j < oldCap; ++j) {
... table 에 oldTab 데이터 복제.
}
2-1. resize() : size 가 늘어난 newTab 생성
2-2. resize() : table 을 newTab 으로 변경 --- (3)
2-3. resize() : table 에 oldTab 데이터 복제. --- (4)
2-4. resize() : 리사이즈 완료.
2-2 ~ 2-3 은 새로운 HashTable 이 table 로 할당되었지만 oldTab 에 있는 데이터들이 온전히 복사 되지 않은 상태이다.
이때 다른 쓰레드가 HashMap.get() 을 하게 되면 HashMap 은 온전하지 않은 newTab 에서 데이터를 찾게 되므로 이전에 put 되었던 key 에 대해서 null 을 리턴해버리게 된다.
3. 다른 쓰레드가 get() 호출
- getNode() 할때 맴버 변수인 table 에서 데이터를 찾는 걸 코드로 확인할 수 있다.
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && --- 1
(first = tab[(n - 1) & hash]) != null) {
....
}
HashMap 는 Thread Safe 하지 않기 때문에 Multi Thread 환경에서 공유된 Map 을 사용할때는 ConcurrentHashMap 사용!