원칙 JDK8의 HashMap, 구현 및 최적화에 대한 심층 분석

의 HashMap은 키와 null 값의 삽입을 허용, 삽입의 순서를 보장하지 않습니다, 가장 자주 사용되는 키 매핑 데이터 구조를 처리라고 할 수있다. 본 논문에서는, HashMap의 구현 및 최적화의 원칙의 JDK8 소스, 심층 분석. 마이크로 채널 대중 번호부터 주현절 소스 .

1. 기본 구조

기준 HashMap의 해시 테이블을 이용하여 구현 파스너 방법 JDK8에서 핸들 충돌 8 사슬 길이보다 큰 경우 변환 레드 - 블랙 트리 저장, 다음과 같은 기본 구조이다 :

의 HashMap의 기본 구조

노드 <K를, V> 해시 맵 [표 필드는 다음과 같이, 즉, 해시 버킷 배열의 배열 요소 노드 객체의 구조를 정의한다 :

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash; // 用于计算数组索引
  final K key;
  V value;
  Node<K,V> next; // 后继节点,下一个 Node

  Node(int hash, K key, V value, Node<K,V> next) { ... }
  ...
}
复制代码

해시 버킷 배열을 처음 사용할 때 초기화, 기본 크기는 16이고, 필요에 따라 크기를 조정하고, 길이는 항상 2의 거듭 제곱이다. 초기 용량은 2 세트 생성자의 힘이 아닌 경우, 다음 돌아가 다음 방법을 사용하여 더 이상 및 가장 가까운 두의 전력 값 :

static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
复制代码

원리는 (1) 전체 세트의 오른쪽으로 1 비트, 다음 1 추가 모든 비트의 최고이며, 모두 우측으로 1 비트에 최상위 비트는 2의 거듭 제곱의 값에 도달하기 위해 0이된다. N을 사용하여 계산이 마지막으로 JDK7 Integer.highestOneBit (INT의 ⅰ) 방법에서 사용되는 - (N >>> 1)로 반환 작은보다는과 가까운 두 힘의 파라미터.

내부 HashMap의 다른 필드 :

// 键值对的数量
transient int size;
// 记录结构修改次数,用于迭代时的快速失败
transient int modCount;
// 负载因子,默认 0.75f
final float loadFactor;
// 扩容的下一个容量值,也就是键值对个数的最大值,它等于(capacity * loadFactor)
int threshold;
复制代码

: 성능에 영향을 미치는 주요 매개 변수의 HashMap이다 초기 용량부하 계수 . 해시 테이블의 요소 수는 부하율 및 원래의 용량 확대 될 현재 증설 제품 초과 할 경우 이중 , 다시 해시의 열쇠.

  • 초기 용량을 여러 번 확장과 재탕을 트리거 너무 작아, 그래서 더 효과적으로 충분히 큰 용량을 미리 할당
  • 하중 계수 기본값은 일반적으로 수정하지 않고, 더 높은 값이 공간 오버 헤드를 줄일 수, 시간과 공간 비용 사이의 적절한 균형, 0.75f이지만, 찾고 비용을 증가

아무리 합리적인 해싱 알고리즘, 상황은,하여 HashMap의 성능에 영향을 미치는 따라서, JDK8 체인 길이는 빠른 CRUD을 활용하기 위해 레드 - 블랙 트리, 레드 - 블랙 트리로 변환되는, 8보다 큰 불가피하게 긴 체인없는 때 기능을 제공합니다.

(2) 해시 함수

가장 일반적으로 사용되는 방법의 정수 해시는 내가 머물 제외 . 균일 해시 값의 해시 키 위해, 어레이 촬영 보통 크기 소수 (즉, 해시 테이블 (11)의 초기 크기) I 작은 확률의 수와 동일한 소인수의 소수, 충돌의 가능성이 작기 때문에.

HashMap의 용량은 항상 두 개의의 힘이다 합성 수 작업으로 모듈로 연산을 배치하고 성능을 향상시키기 위해,이 디자인에 대한 이유를. 이 방정식 h % length = h & (length-1)은 다음과 같이 설정 이유 :

2^1 = 10          2^1 -1 = 01 
2^2 = 100         2^2 -1 = 011 
2^3 = 1000        2^3 -1 = 0111
2^n = 1(n个零)     2^n -1 = 0(n个1) 
复制代码

우측 2 ^ N 이진 특징, 특성이 왼쪽 ^ N-1이 발견 될 수있을 때의 시간 길이 = 2 ^ N, H 및 (길이 1) 정확한 길이 1 0 사이에있는 결과, 매우 모듈로 동작한다.

조작 위치 켠 후, 길이 (1)는 동등 낮은 마스크 , 가압 위치 높은 위치에이를 것이다 해시 마스크 단지 작은 영역에서의 변화를 초래 원래 해시 값 0, 분명히 충돌의 가능성이 높아집니다. 충돌, 디자인에 HashMap의 해시 알고리즘의 사용을 줄이기 위해 높고 낮은 비트 XOR을 , 또한 작업과 관련된 그래서 높은 결합을 위장, 코드는 다음과 같다 :

static final int hash(Object key) { // JDK8
  int h;
  // h = key.hashCode()  1. 取hashCode值
  // h ^ (h >>> 16)      2. 高16位与低16位异或,变相保留高位的比特位
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// JDK7 的源码,JDK8 没有这个方法,但原理一样
static int indexFor(int h, int length) {
   return h & (length-1); // 3. 取模运算
}
复制代码

높고 낮은 XOR 높은 변위, 키 정보의 효율적인 사용을 보장 할뿐만 아니라, 시스템 오버 헤드를 줄이기 위해,이 디자인 속도, 효율성 및 품질 사이의 트레이드 오프이다.

3. 풋 운영

다음과 같은 일을 주로 운영 넣어 :

  1. 해시 테이블 어레이 버킷이 비어는, 상기 방법은 () 크기 조정에 의해 초기화
  2. 키는 이미 덮어 쓰기 값이있는 삽입되는
  3. 그렇지 않은 경우, 키는 해당 목록이나 레드 - 블랙 트리에 삽입
  4. 삽입은 레드 - 블랙 트리 목록을 설정 여부 결정
  5. 용량이 있는지 여부를 확인하기 위해 필요

다음과 같이 핵심 코드는 다음과 같습니다

public V put(K key, V value) {
  // 将 key 的 hashCode 散列
  return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 1. table 为 null,初始化哈希桶数组
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // 2. 计算对应的数组下标 (n - 1) & hash
  if ((p = tab[i = (n - 1) & hash]) == null)
    // 3. 这个槽还没有插入过数据,直接插入
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    // 4. 节点 key 存在,直接覆盖 value
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    // 5. 该链转成了红黑树
    else if (p instanceof TreeNode) // 在树中插入
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    // 6. 该链是链表
    else {
      for (int binCount = 0; ; ++binCount) {
        // 遍历找到尾节点插入
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          // 链表长度大于 8 转为红黑树
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        // 遍历的过程中,遇到相同 key 则覆盖 value
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  // 7. 超过最大容量,扩容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}
复制代码

헤드 JDK7가 역방향 삽입 보간을 사용하는 동안, 순서가 삽입되는 삽입 방법 목록 끝에 삽입되는 경우 JDK8 사용.

6. 팽창기구

기본적으로 초기 용량 부하율 0.75f, 임계 값 (12), 즉 키 (12)의 삽입이 팽창 할 것이다 (16)이다.

이 팽창에 두번 일본어, 팽창 될 때 사용하기 때문에 전원 확장 2 다음 중 하나는 원래의 위치에 파워 시프트 변하지, 또는 제 2 엘리먼트의 위치.

크기를 조정-1

그림 2에 의해 증폭, 알 수있는 왼쪽 하나는 N-1 (1)의 이상에 높은 수준이어서, N에 대응하는 원래의 해시 값의 계산은 더 복잡 하나, 이번에 이렇게 비트는 0 또는 1 :

  • 0 인덱스를 변경하는 경우
  • 그런 인덱스 1이된다 "원래의 인덱스 + oldCap"

어떻게이 비트가 0 또는 1 그것입니다 결정하기 위해? 은 "원래의 해쉬 값 oldCap"가 0이면, 비트가 0 인 것을 나타낸다. 다음과 같이 확장 코드는 다음과 같습니다

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    // 超过最大值,不在扩容
    if (oldCap >= MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }// 否则扩大为原来的 2 倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
           oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    // 初始化时,threshold 暂时保存 initialCapacity 参数的值
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  // 计算新的 resize 上限
  if (newThr == 0) {
      float ft = (float)newCap * loadFactor;
      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
  }
  threshold = newThr;
  @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  // 将旧的键值对移动到新的哈希桶数组中
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null) // 无链条
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
          // 拆红黑树,先拆成两个子链表,再分别按需转成红黑树
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
          // 拆链表,拆成两个子链表并保持原有顺序
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
          do {
            next = e.next;
            // 原位置不变的子链表
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            // 原位置偏移 oldCap 的子链表
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          // 放到新的哈希桶中
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}
复制代码

목록 엘리먼트의 위치를 재 계산하여 구하는 만 가능하면 두 개의 하위리스트 :리스트 인덱스의 동일한 구성 요소 목록의 동일한 오프셋 요소를 가지고있다. 서브리스트의 구성 동안, 헤드 노드 테일 노드는 분할 후 순차적 보장 :

크기를 조정-2

참조 TreeNode.split () 메소드는 분할이 완료된 후 레드 - 블랙 트리 분할 및 논리적 연결리스트처럼,하지만, 다음과 같은 처리가 하위 목록의 길이에 따라 수행된다는 것을 보여 주었다 :

  • 일반적인 체인 길이가 6 이하, 그것의 TreeNode의 반환을 포함하지 않습니다
  • 레드 - 블랙 트리에 그렇지 않으면, 하위 목록

레드 - 블랙 트리 차례도 촉진 할 참조 체인의 원래 목록을 유지하는 경우 때문에 논리적으로 연결리스트를 분할 할 수 레드 - 블랙 트리, 링크 된 목록입니다 탐색 작업을.

7. 목록 설정 블랙 트리

전송 목록 레드 - 블랙 트리는 다음과 같은 일을 주로 :

  1. 나무의 최소 요구 사항 여부, 또는 확장에 대한 판단 탱크 용량
  2. TreeNode를로 구성된 이중 연결리스트에 원래 목록
  3. 레드 - 블랙 트리에 새 목록

다음과 같이 코드입니다 :

final void treeifyBin(Node<K,V>[] tab, int hash) {
  int n, index; Node<K,V> e;
  // 如果哈希桶容量小于树化的最小容量,优先进行扩容
  if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();
  else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do { // 将普通节点转为树形节点
      TreeNode<K,V> p = replacementTreeNode(e, null);
      if (tl == null)
        hd = p;
      else {
        p.prev = tl;
        tl.next = p;
      }
      tl = p;
      // 把原来的单链表转成了双向链表
    } while ((e = e.next) != null);
    if ((tab[index] = hd) != null)
      hd.treeify(tab); // 将链表转为红黑树
  }
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
  return new TreeNode<>(p.hash, p.key, p.value, next);
}
复制代码

HashMap의가 레드 - 블랙 트리를 후 도입의 디자인으로 간주되어서는 안됩니다, 그것은 키 비교를 제공하거나 키가 Comparable 인터페이스를 구현 필요하지 않습니다. 다음 단계를 처리하는 해시 맵, 두 개의 키 크기를 비교하려면 :

  1. 두 가지 주요 범위의 해시 값을 비교 해시 값의 크기의 경우
  2. 키 비교 방법을 사용 compareTo와 비교 가능한 인터페이스를 달성하는 경우, 그들이 동일한 경우
  3. 결과가 동일하면, 다음과 같이 정의 자기 tieBreakOrder 비교 논리를 사용하는 방법은
static int tieBreakOrder(Object a, Object b) {
  int d;
  if (a == null || b == null || // 比较 className 的大小
    (d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)
    // 比较由本地方法生成的 hash 值大小,仍然有可能冲突,几率太小,此时认为是小于的结果
    d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
  return d;
}
复制代码

8. 요약

코드에 JDK8의 HashMap은 매우 복잡, 다음과 같은 세 가지 점에서 주로 최적화 :

  • 최적화 해시 알고리즘은 하나의 시프트 작업입니다
  • 레드 - 블랙 트리의 소개가 더 심각한 경우에 충돌, 가져 오기 작업의 복잡도 O의 시간 (n)은 O로 감소 (logn)
  • 확장 2의 이진 값의 전력 특성을 사용하는 경우에만 다시 다른 위치 노드 해시에 충돌하기 전에, 해시 시간을 계산 할 필요가 없습니다

또한, 해시 MAP되는 안전하지 스레드 , 스레드 간의 경쟁 상태 주로 때 충돌 또는 확장 사슬 절단 연속 동작의리스트. 확장은 매우 시간 소모적 인 동작 성능 메모리 복사, 그래서 더 나은 성능을 위해 해시 MAP 있도록 확장의 수를 줄여, 충분히 큰 초기 용량을 사전에 할당하는 것을 의미한다.

검색 마이크로 채널 대중 번호 " 주현절 소스 코드를 더 소스 코드 분석을위한"와 바퀴를 구축 할 수 있습니다.

추천

출처juejin.im/post/5d7ec6d4f265da03b76b50ff