HashMap 내부 구조 깊이 분석

11505 단어
java. util. HashMap 은 흔히 볼 수 있 는 유형 입 니 다. 얼마 전에 회사 시스템 이 HashMap 에 대한 사용 이 부적 절하 여 cpu 가 100% 병발 환경 에서 HashMap 을 사용 하여 동기 화 되 지 않 았 기 때문에 사 순환 을 일 으 킬 수 있 습 니 다. 이 점 에 대해 sun 의 공식 사이트 에서 설명 한 바 와 같이 이것 은 bug 가 아 닙 니 다.
HashMap 의 데이터 구조 HashMap 은 주로 배열 로 데 이 터 를 저장 합 니 다. 우 리 는 모두 key 에 대해 해시 연산 을 할 것 이라는 것 을 알 고 있 습 니 다. 하 계 연산 은 중복 되 는 해시 값 이 있 고 해시 값 의 충돌 에 대해 HashMap 은 링크 로 해결 합 니 다.HashMap 에 이러한 속성 성명 이 있 습 니 다: transient Entry [] table;Entry 는 HashMap 에 저 장 된 데이터 입 니 다. 속성 은 다음 과 같 습 니 다.
1
2
3
4
final K key;
 V value;
 final int hash;
 Entry<K,V> next;

넥 스 트 보이 시 나 요?next 는 해시 충돌 을 위해 존재 한다.예 를 들 어 해시 연산 을 통 해 새로운 요 소 는 배열 의 10 번 째 위치 에 있어 야 하지만 10 번 째 위 치 는 Entry 가 있 습 니 다. 그러면 좋 습 니 다. 새로 추 가 된 요 소 를 10 번 째 위치 에 두 고 10 번 째 위치의 기 존 Entry 를 현재 새로 추 가 된 Entry 의 next 속성 에 할당 합 니 다.배열 에 저 장 된 링크, 링크 는 하 쉬 충돌 을 해결 하기 위 한 것 이 므 로 주의해 야 한다.몇 개의 관건 적 인 속성 으로 데 이 터 를 저장 하 는 배열 transient Entry [] table;이 위 에 기본 용량 이 언급 되 어 있 습 니 다.
1
static final int DEFAULT_INITIAL_CAPACITY = 16;

최대 용량
1
static final int MAXIMUM_CAPACITY = 1 << 30;

기본 로드 인자, 로드 인 자 는 하나의 비율 입 니 다. HashMap 의 데이터 크기 > = 용량 * 로드 인 자 를 불 러 올 때 HashMap 은 용량 을 확장 합 니 다.
1
static final float DEFAULT_LOAD_FACTOR = 0.75f;

실제 데이터 크기 가 threshold 를 초과 할 때 HashMap 은 용량 을 확대 합 니 다. threshold = 용량 * 로드 인자
1
int threshold;

로 딩 인자
1
final float loadFactor;

HashMap 의 초기 프로 세 스 구조 함수 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);         // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;         this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

여기 주목 해.
1
2
while (capacity < initialCapacity) 
            capacity <<= 1;

capacity 야 말로 initialCapacity 가 아니 라 초기 용량 입 니 다. 이것 은 new HashMap (9, 0.75) 을 실행 하면 특히 주의해 야 합 니 다.그럼 HashMap 의 초기 용량 은 9 가 아니 라 16 입 니 다. 왜 그런 지 생각해 보 세 요.
구조 함수 2
1
2
3
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

구조 함수 3, 모두 기본 값
1
2
3
4
5
6
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

구조 함수
1
2
3
4
5
public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

어떻게 해시 해시 맵 은 대상 의 hashcode 를 해시 값 으로 하 는 것 이 아니 라 key 의 hashcode 를 연산 하여 최종 해시 값 을 얻 고 얻 은 해시 값 도 배열 의 위치 가 아 닙 니 다. get 이 든 put 이 든 다른 방법 이 든 해시 값 을 계산 하 는 것 은 이 문장 입 니 다.
1
int hash = hash(key.hashCode());

hash 함 수 는 다음 과 같 습 니 다:
1
2
3
static int hash(int h) {
    return useNewHash ? newHash(h) : oldHash(h);
    }

useNewHash 성명 은 다음 과 같 습 니 다:
1
2
private static final boolean useNewHash;
    static { useNewHash = false; }

이것 은 useNewHash 가 사실 줄곧 false 이 고 바 꿀 수 없 는 것 을 설명 한다. hash 함수 에서 useNewHash 에 대한 판단 은 정말 불필요 하 다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static int oldHash(int h) {
        h += ~(h << 9);
        h ^=  (h >>> 14);
        h +=  (h << 4);
        h ^=  (h >>> 10);
        return h;
    }
    private static int newHash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

사실 HashMap 의 해시 함 수 는 항상 oldHash 일 것 입 니 다.
데이터 의 위 치 를 확인 하면 아래 두 줄 을 보 세 요.
1
2
int hash = hash(k.hashCode());int i = indexFor(hash, table.length);

첫 번 째 줄 은 위 에서 말 했 듯 이 해시 값 을 얻 는 것 이 고 두 번 째 줄 은 해시 가 가리 키 는 요소 가 배열 에 있 는 위 치 를 계산 하 는 것 이다. 위치의 계산 은 해시 값 과 배열 의 길 이 를 위치 와 연산 하 는 것 이다.
1
2
3
static int indexFor(int h, int length) {
        return h & (length-1);
    }

put 방법 은 도대체 무엇 을 했 습 니까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        } 
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

키 가 NULL 이면 단독으로 처리 합 니 다. puutForNullKey 방법 을 보십시오.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 private V putForNullKey(V value) {
        int hash = hash(NULL_KEY.hashCode());
        int i = indexFor(hash, table.length);         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.key == NULL_KEY) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        } 
        modCount++;
        addEntry(hash, (K) NULL_KEY, value, i);
        return null;
    }

NULL_KEY 의 성명:
1
static final Object NULL_KEY = new Object();

이 코드 는 해시 충돌 을 처리 하 는 것 입 니 다. 즉, 배열 의 특정한 위치 에 있 는 대상 이 유일한 것 이 아 닐 수도 있 습 니 다. 이것 은 링크 구조 입 니 다. 해시 값 에 따라 링크 를 찾 은 후에 링크 를 옮 겨 다 니 며 key 와 같은 대상 을 찾 아 교체 하고 오래된 값 으로 돌아 가 야 합 니 다.
1
2
3
4
5
6
7
8
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.key == NULL_KEY) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

이 위 치 를 옮 겨 다 니 는 링크 가 key 와 같은 것 을 찾 지 못 하면 현재 대상 을 링크 에 추가 합 니 다.
1
2
3
 modCount++;
  addEntry(hash, (K) NULL_KEY, value, i);
  return null;

addEntry 방법 도 보 세 요.
1
2
3
4
5
6
  void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

table[bucketIndex] = new Entry(hash, key, value, e);Entry 대상 을 새로 만 들 고 현재 위치 에 있 는 Entry 링크 의 머리 에 놓 습 니 다. 아래 의 Entry 구조 함 수 를 보면 알 수 있 습 니 다. 빨간색 부분 에 주의 하 세 요.
1
2
3
4
5
6
Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n; 
            key = k;
            hash = h;
        }

어떻게 용량 을 늘 립 니까?put 원 소 를 사용 할 때 용량 제한 에 도달 하면 HashMap 은 용량 을 늘 리 고 새로운 용량 은 영원히 원래 의 2 배 입 니 다.위의 put 방법 에는 다음 과 같은 부분 이 있 습 니 다.
1
2
if (size++ >= threshold)
            resize(2 * table.length);

이것 은 확장 판단 이다. 데이터 크기 가 HashMap 의 최대 용량 에 이 를 때 만 확장 되 는 것 이 아니 라 threshold 가 지정 한 값 에 이 를 때 확장 되 기 시작 하 는 것 이다. threshold = 최대 용량 * 로드 인자.resize 방법 을 보 세 요.
1
2
3
4
5
6
7
8
9
10
11
12
13
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        } 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable); 
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

빨간색 파 트 의 transfer 방법 을 중점적으로 살 펴 보 겠 습 니 다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);  
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

tranfer 방법 은 모든 요 소 를 다시 해시 합 니 다. 새로운 용량 이 커지 기 때문에 모든 요소 의 해시 값 과 위 치 는 다 릅 니 다.
하 쉬 맵 1: 동시 다발 장면 에서 하 쉬 맵 을 사용 하지 마 세 요. 하 쉬 맵 은 스 레 드 가 안전 하지 않 습 니 다. 여러 스 레 드 에 의 해 공 유 된 작업 을 하면 예측 할 수 없 는 문 제 를 일 으 킬 수 있 습 니 다. sun 에 따 르 면 확장 할 때 링크 의 폐쇄 를 일 으 킬 수 있 고 get 요 소 를 할 때 무한 순환 을 일 으 키 며 결 과 는 cpu 100% 입 니 다.get 방법의 빨 간 부분 을 보 겠 습 니 다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public V get(Object key) {
    if (key == null)
        return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

2: 데이터 크기 가 고정 되 어 있다 면 HashMap 에 합 리 적 인 용량 값 을 설정 하 는 것 이 좋 습 니 다.
위의 분석 에 따 르 면 HashMap 의 초기 기본 용량 은 16 이 고 기본 로드 인 자 는 0.75 입 니 다. 즉, HashMap 의 기본 구조 함 수 를 사용 하면 데 이 터 를 증가 할 때 데이터 의 실제 용량 이 10 * 0.75 = 12 를 초과 할 때 HashMap 은 확장 되 고 확장 은 일련의 연산 을 가 져 옵 니 다. 원래 용량 의 2 배 인 배열 을 새로 만 들 고 기 존의 요 소 를 모두 다시 해시 합 니 다.만약 당신 의 데이터 가 수천 수만 개가 있 고 기본 적 인 HashMap 구조 함 수 를 사용한다 면 그 결 과 는 매우 비극 적 입 니 다. HashMap 이 계속 확대 되 고 해시 가 끊 이지 않 기 때 문 입 니 다. HashMap 을 사용 하 는 장면 에서 여러 스 레 드 가 HashMap 을 공유 하지 않 을 것 입 니 다. HashMap 포장 과 동기 화 되 지 않 는 한 이 로 인해 발생 하 는 메모리 비용 과 cpu 비용 은 어떤 상황 에서 치 명 적일 수 있 습 니 다.

좋은 웹페이지 즐겨찾기