본문 바로가기
혼자 공부하는 것들/JAVA

ConcurrentHashMap의 get(), put() 분석

by applepick 2023. 7. 2.
반응형

동시성 문제를 해결할 수 있는 ConcurrentHashMap이 있습니다. 동시성을 지원하며, 여러 스레드가 동시에 안전하게 접근할 수 있는 해시 맵입니다. 내부적으로 세그먼트라고 하는 여러 개의 부분 맵으로 나뉘어져 있습니다. 각 세그먼트는 서로 독립적으로 동작하며, 동시에 여러 스레드가 각각의 세그먼트에 접근할 수 있습니다. 각 세그먼트는 독립적인 잠금을 가지고 있으므로, 동시에 여러 스레드가 다른 세그먼트에 접근하여 작업을 수행할 수 있습니다.

 

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

ConcurrentHashMap의 get() 메서드가 호출되면, 주어진 키에 대한 해시 함수를 통해 해시 값을 계산합니다. 이 해시 값은 ConcurrentHashMap 내부의 배열 버킷에 대한 인덱스로 사용됩니다. 계산된 해시 값을 바탕으로 ConcurrentHashMap의 내부 배열 버킷에 접근합니다. 이때, 다중 스레드 환경에서 동시 접근을 지원하기 위해 해당 버킷에 잠금(락)을 걸 수 있습니다.
버킷의 잠금은 다른 스레드가 동시에 해당 버킷을 수정하는 것을 방지합니다. 접근한 버킷 내에서 키와 일치하는 항목을 찾습니다. 버킷은 일반적으로 연결 리스트 또는 트리 구조로 구성되어 있습니다. 연결 리스트인 경우, 순차적으로 항목을 비교하여 일치하는 키를 찾습니다. 동시성을 지원하기 위해 ConcurrentHashMap은 세그먼트라는 작은 부분 맵으로 분할되어 있으며, 각 세그먼트는 독립적으로 동작하여 동시 접근을 가능하게 합니다.

 

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;
    }

ConcurrentHashMap에서 값을 추가할 경우 putVal() 메서드를 기본적으로 사용합니다. putVal() 메서드가 호출되면, 주어진 키에 대한 해시 함수를 통해 해시 값을 계산합니다.
이 해시 값은 ConcurrentHashMap 내부의 배열 버킷에 대한 인덱스로 사용됩니다. 계산된 해시 값을 바탕으로 ConcurrentHashMap의 내부 배열 버킷에 접근합니다. 이때, 다중 스레드 환경에서 동시 접근을 지원하기 위해 해당 버킷에 잠금(락)을 걸 수 있습니다. 버킷의 잠금은 다른 스레드가 동시에 해당 버킷을 수정하는 것을 방지합니다. 일치하는 키를 찾은 경우, 해당 항목의 값을 업데이트합니다. 일치하는 키가 없는 경우, 새로운 항목을 추가합니다. 이때, 다중 스레드 환경에서의 안전성을 위해 적절한 동기화 메커니즘을 사용하여 버킷 레벨 또는 맵 레벨에서의 동시 접근을 관리합니다. putVal() 메서드를 통해 항목을 추가하면서, ConcurrentHashMap의 크기가 일정 기준을 초과하는 경우 리사이징이 발생할 수 있습니다. 리사이징은 ConcurrentHashMap의 성능과 동시성을 유지하기 위해 내부 배열의 크기를 확장하거나 축소하는 과정입니다.

 

반응형

댓글