Java 1.8 HashMap小结

关于JDK1.6、1.7、1.8三个版本,HaspMap的实现是有区别的,特别是1.8,对hashmap的结构进行了较大的变化。

1.6整体采用的是位桶+链表的方式,而1.8使用的是位桶+链表+红黑树实现,链表的阈值是8,超过后就由链表变成红黑树,大大增加了查询的效率,HashMap结构图

hashmap1.png
HashMap源码结构
//Node是单向链表,它实现了Map.Entry接口  
static class Node implements Map.Entry {  
    final int hash;  
    final K key;  
    V value;  
    Node next;  
    //构造函数Hash值 键 值 下一个节点  
    Node(int hash, K key, V value, Node next) {  
        this.hash = hash;  
        this.key = key;  
        this.value = value;  
        this.next = next;  
    }  
  
    public final K getKey()        { return key; }  
    public final V getValue()      { return value; }  
    public final String toString() { return key + = + value; }  
  
    public final int hashCode() {  
        return Objects.hashCode(key) ^ Objects.hashCode(value);  
    }  
  
    public final V setValue(V newValue) {  
        V oldValue = value;  
        value = newValue;  
        return oldValue;  
    }  
    //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true  
    public final boolean equals(Object o) {  
        if (o == this)  
            return true;  
        if (o instanceof Map.Entry) {  
            Map.Entry e = (Map.Entry)o;  
            if (Objects.equals(key, e.getKey()) &&  
                Objects.equals(value, e.getValue()))  
                return true;  
        }  
        return false;  
    }  
}  

//红黑树  
static final class TreeNode extends LinkedHashMap.Entry {  
    TreeNode parent;  // 父节点  
    TreeNode left;  //左子树  
    TreeNode right;//右子树  
    TreeNode prev;    // needed to unlink next upon deletion  
    boolean red;    //颜色属性  
    TreeNode(int hash, K key, V val, Node next) {  
        super(hash, key, val, next);  
    }  
    //返回当前节点的根节点  
    final TreeNode root() {  
        for (TreeNode r = this, p;;) {  
            if ((p = r.parent) == null)  
                return r;  
            r = p;  
        }  
    }  
}  

transient Node[] table;//存储(位桶)的数组  

以上3个数据结构,可以大致联想到HashMap的实现。首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,所以说数组存放的是链表。而当链表长度超过8时,链表就转换为红黑树。

Hashap构造函数
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);  
  this.loadFactor = loadFactor;  
  // 新的扩容值  
  this.threshold = tableSizeFor(initialCapacity);  
}  

Map中,threshold = initialCapacity * loadFactor;所以在这里面,loadFactor越小,占用的空间就越多,查询检索的效率就越高;反之loadFactor越大,占用空间就小,而查询检索效率就低。由于haspMap的定义就是空间换时间,loadFactor负载因子值设置非常重要。

默认情况下,loadFactor为0.75,threshold 为12;如果loadFactor为1,threshold 就是16,为什么说loadFactor越大,空间占用小?比如一个为15的Map放入,那么在loadFactor在0.75时,threshold 12是需要是需要扩容一倍的,此时初始空间为32;而为1的情况下不需要扩容
,所以占用空间就小。扩容后,位桶table也会增大,为旧的两倍,对比不扩容,位桶上hash冲突会减少很多,检索查询效率自然会高些。

HashMap扩容机制

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length),就会进行扩容,由于扩容存在数组复制,扩容是比较耗费时间的。

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;  
      }  
      // 旧的容量翻倍后没有到极限值,且大于等于默认值  
      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
               oldCap >= DEFAULT_INITIAL_CAPACITY)  
          newThr = oldThr << 1; //扩容值翻倍  
  }  
  else if (oldThr > 0) // initial capacity was placed in threshold  
      newCap = oldThr;  
  else {               // 未设定使用默认值  
      newCap = DEFAULT_INITIAL_CAPACITY;  
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
  }  
  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 { // 链表时,需要复制新的  
                  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;  
                      }  
                      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;  
}
put/get 说明
  1. 判断键值对数组tab[]是否为空或为null,否则resize();

  2. 根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3

  3. 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理。

     static final int hash(Object key) {  
       int h;  
       return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
     }  
    
     public V put(K key, V value) {  
       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;  
       if ((tab = table) == null || (n = tab.length) == 0)  
           n = (tab = resize()).length; //table为空的时候,n为table的长度  
       if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 与1.6中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。  
           tab[i] = newNode(hash, key, value, null);   
       else {  
           // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同  
           Node<K,V> e; K k;  
           if (p.hash == hash &&  
               ((k = p.key) == key || (key != null && key.equals(k))))  
               e = p;//相同则覆盖之  
           else if (p instanceof TreeNode)  
           // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。  
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  
           else {  
           // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。  
               for (int binCount = 0; ; ++binCount) {  
                   if ((e = p.next) == null) {  
                       p.next = newNode(hash, key, value, null);  
                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                           treeifyBin(tab, hash);  
                       break;  
                   }  
                   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;  
       if (++size > threshold)  
           resize();  
       afterNodeInsertion(evict);  
       return null;  
      }
    
  4. 代码 (n - 1) & hash; hash先由key值通过hash(key)获取,再通过 hash &(n即为length-1)得到所在数组位置。一般对于哈希表的散列常用的方法有直接定址法,除留余数法等,既要便于计算,又能减少冲突,但是取模中的除法运算效率很低,所以HashMap则通过hash &(length-1)。

hashmap2.png

一是保证使用 和运算时能够最大的在位桶上均分hash值,减少hash冲突,上图在未-1情况下,都只有一种结果;数组的length永远是2的N次方,那么length-1最后转成2进制,最后一位都是1,& 操作就可能出现0 或 1,如果不减1,那么2进制最后一位永远都是0,& 的运算结果只有0,明显增加了hash的冲突;
二是保证了结果都在length的长度内,& 操作后,结果永远都是 <= length 或者length -1,但在极端情况下出现 Node[length],就会出现问题,而Node[length -1]不会。

在HashMap实现中还可以看到如下代码取代了以前版本JDK1.6的while循环来保证哈希表的容量一直是2的整数倍数,用移位操作取代了循环移位。

//这段代码保证HashMap的容量总是2的n次方  
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;  
}  

ConcurrentHashMap 总结

  1. ConcurrentHashmap 使用的是位桶+链表/红黑树实现,结构与hashmap一样,它摒弃了以前Segment(锁段)的概念,而用了一种新的方式实现,CAS算法实现,并且为了实现并发,加入了如treeBin等辅助类;treeBin并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象

  2. Node节点类中与hashmap不一样的是,在对value和next属性设置了volatile同步锁,不允许调用setValue方法直接改变Node的value域,但增加了find方法。

  3. 一个重要的属性sizeCtrl用来表示扩容的情况的标识,正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小;-1代表正在初始化;-N 表示有N-1个线程正在进行扩容操作。

  4. put操作

第一步:初始化,对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候。

第二步:在存入值前,key值hash化,遍历位桶数组,如果发现需要扩容,则首先扩容。构建一个2倍中间的nexttable表,在需要复制的地方,先将一个null节点设置为ForwardingNode,然后加锁,复制内容;其他线程遍历到这个节点时,就会跳过往下一个节点继续遍历,直到复制玩内容,sizeCtrl变成扩容后的0.75n

第三步:如果当前hash没被占用,会被直接存储。如果hash有冲突,若当前节点下为链表,则会依次比对,hash和key都相同,则替换value值;没有就将当前值插入到链表最后,当发现链表长多超过8时,会转成红黑树,但在位桶中存的是TreeBin,TreeBin存放的是TreeNode。如果有冲突且节点下为treeBin时,则按照红黑树查找,存放到相应位置。

  1. get操作,get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找。

  2. size对于ConcurrentHashMap来说,这个table里到底装了多少东西其实是个不确定的数量,因为不可能在调用size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值,主要运用了mappingCount!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351