티스토리 뷰

JAVA

JAVA HashMap 과 동시성 #1 (Pub/Sub)

소농배 2021. 6. 4. 13:50
 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 를 잡았을때 상태

(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 상태

put 하기 전

 

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 생성

newTab 생성

2-2. resize() : table 을 newTab 으로 변경 --- (3)

pointer 교체

2-3. resize() : table 에 oldTab 데이터 복제. --- (4)

copying

 

2-4. resize() : 리사이즈 완료.

resize() 완료

 

 

2-2 ~ 2-3 은 새로운 HashTable 이 table 로 할당되었지만 oldTab 에 있는 데이터들이 온전히 복사 되지 않은 상태이다.

이때 다른 쓰레드가 HashMap.get() 을 하게 되면 HashMap 은 온전하지 않은 newTab 에서 데이터를 찾게 되므로 이전에 put 되었던 key 에 대해서 null 을 리턴해버리게 된다.

 

 

3. 다른 쓰레드가 get() 호출

 - getNode() 할때 맴버 변수인 table 에서 데이터를 찾는 걸 코드로 확인할 수 있다.

다른 Thread 가 아직 복사 되지 않은 [3] 에 접근

    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 사용!

 

 

 

 

 

'JAVA' 카테고리의 다른 글

mariadb-connector-j Aurora read/write seperate 과정  (0) 2021.10.08
자바 separate chaining in HashMap  (0) 2021.06.04
G1GC ( Garbage First ) GC  (0) 2020.05.06
CMS Collector GC  (0) 2020.05.06
Java Garbage Collector 의 종류  (0) 2020.05.06
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함