Java8---4.对HashMap和ConcurrentHashMap的改进

来源:互联网 发布:手机号批量加微信软件 编辑:程序博客网 时间:2024/05/16 17:37

Java8为何比Java7速度更快,从底层数据结构开始说起

1.HashMap

在Java7中,HashMap采用的是数组+链表的形式存储的,(默认bucket数目16,负载因子0.75)

具体见 HashMap


那么依照原HashMap,假设bucket=12的数组对应的链表中有10个元素(此时负载仍没超过0.75,10/16)那么在查找该bucket的过程中最坏的情况是要比较10次。(删除操作也是)
如果当超过负载,就要rehash,那么此过程,就是要对bucket=12中的每一个元素都要重新计算hash值,在放到新的扩容后的bucket中,这里请注意是每个元素。

以上两种情况下,就会比较耗时,因此Java8改进了HashMap

(2)Java8的HashMap:数组+链表+红黑树

Java8采用了数组+链表+红黑树的存储结构,首次还是按照数组+链表的存储方式,当某个bucket同时满足以下两条时,将其链表转化成红黑树(16,0.75):

(a)当某个bucket对应的链表元素个数>8

(b)HashMap总大小(总元素个数)>64

如果红黑树中的元素个数<6就将其转化成链表

新结构如图:


优点:对于bucket=4,除了插入操作意外,所有操作都加快了

原有链表的插入是在表头,就是bucket的位置。而新的插入,要进行红黑树的插入,时间复杂度为logn(n是该bucket对应的元素个数)

但是查找、删除操作红黑树都是logn,而链表是n,所以其他的操作都是红黑树优先于链表,也就是Java8更快。

rehash操作:对于原链表,rehash需要对每个元素重新计算,但是采用红黑树中的元素,新bucket=旧bucket+原数组长度

比如图中黄线标记的元素,它的新bucket=4+16=20

final Node<K,V>[] resize() {        。。。        if (oldTab != null) {            for (int j = 0; j < oldCap; ++j) {                。。。                    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;                       。。。                    }        }        return newTab;    }

虽然是一个小小的改进,但意义重大:

O(n)到O(logn)的时间开销。

如果恶意程序知道我们用的是Hash算法,则在纯链表情况下,它能够发送大量请求导致哈希碰撞,然后不停访问这些key导致HashMap忙于进行线性查找,最终陷入瘫痪,即形成了拒绝服务攻击(DoS)。但是使用后者之后,能够有效的防止此类攻击,对该攻击所产生的影响会减弱,同时也让HashMap性能的可预测性有了增强。

注意:既然有了排序树,则key一定需要排序,那么Key类就需要有Comparable接口进行辅助排序。这是早期版本中key所不具有的。


(3)Node和TreeNode

Java8中HashMap中的元素有两种情况,链表中的Node,和红黑树中的TreeNode。TreeNode的属性多了很多

static class Node<K,V> implements Map.Entry<K,V> {        final int hash;        final K key;        V value;        Node<K,V> next;

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {        TreeNode<K,V> parent;  // red-black tree links        TreeNode<K,V> left;        TreeNode<K,V> right;        TreeNode<K,V> prev;    // needed to unlink next upon deletion
而查找、删除、插入等操作也对node和treenode采用了不同的方法
(4)内存

Java7

使用HashMap会消耗一些内存。在Java 7中,HashMap将键值对封装成Entry对象,一个Entry对象包含以下信息:

  • 指向下一个记录的引用
  • 一个预先计算的哈希值(整数)
  • 一个指向键的引用
  • 一个指向值的引用

此外,Java 7中的HashMap使用了Entry对象的内部数组。假设一个Java 7 HashMap包含N个元素,它的内部数组的容量是CAPACITY,那么额外的内存消耗大约是:sizeOf(integer)* N + sizeOf(reference)* (3*N+C)

整数的大小是4个字节,引用的大小依赖于JVM、操作系统以及处理器,但通常都是4个字节。这就意味着内存总开销通常是16 * N + 4 * CAPACITY字节。

注意:

在Map自动调整大小后,CAPACITY的值是下一个大于N的最小的2的幂值

从Java 7开始,HashMap采用了延迟加载的机制。这意味着即使你为HashMap指定了大小,在我们第一次使用put()方法之前,记录使用的内部数组(耗费4*CAPACITY字节)也不会在内存中分配空间。

JAVA 8

在Java 8实现中,计算内存使用情况变得复杂一些,因为Node可能会和Entry存储相同的数据,或者在此基础上再增加6个引用和一个Boolean属性(指定是否是TreeNode)。

如果所有的节点都只是Node,那么Java 8 HashMap消耗的内存和Java 7 HashMap消耗的内存是一样的。

如果所有的节点都是TreeNode,那么Java 8 HashMap消耗的内存就变成:N * sizeOf(integer) + N * sizeOf(boolean) + sizeOf(reference)* (9*N+CAPACITY )在大部分标准JVM中,上述公式的结果是44 * N + 4 * CAPACITY 字节。


2、ConcurrentHashMap

Java7中的ConcurrentHashMap采用了   段+表+链表的形式


每一个segment都是一个HashEntry<K,V>[] table, table中的每一个元素本质上都是一个HashEntry的单向队列。比如table[3]为首节点,table[3]->next为节点1,之后为节点2,依次类推。

  1. public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
  2.         implements ConcurrentMap<K, V>, Serializable {  
  3.   
  4.     // 将整个hashmap分成几个小的map,每个segment都是一个锁;与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对  
  5.     // 不属于同一个片段的节点可以并发操作,大大提高了性能  
  6.     final Segment<K,V>[] segments;  
  7.   
  8.     // 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 可以作为互拆锁使用  
  9.     static final class Segment<K,V> extends ReentrantLock implements Serializable {  
  10.         transient volatile HashEntry<K,V>[] table;  
  11.         transient int count;  
  12.     }  
  13.   
  14.     // 基本节点,存储Key, Value值  
  15.     static final class HashEntry<K,V> {  
  16.         final int hash;  
  17.         final K key;  
  18.         volatile V value;  
  19.         volatile HashEntry<K,V> next;  
  20.     }  
  21. }  

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率,代替原来的每一段加锁。因为段的隔离级别不太容易确定,比如说默认是16,但是很多情况下并不合适,如果太大很多空间就浪费了,如果太小每个段中可能元素过于多,所以取消segments,改成了CAS算法

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

在其他方面也有一些小的改进,比如新增字段 transient volatile CounterCell[] counterCells; 可方便的计算hashmap中所有

元素的个数,性能大大优于jdk1.7中的size()方法。

put方法:

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;          // 如果table为空,初始化;否则,根据hash值计算得到数组索引i,如果tab[i]为空,直接新建节点Node即可。注:tab[i]实质为链表或者红黑树的首节点。          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, null)))                  break;                   // no lock when adding to empty bin          }          // 如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table。          else if ((fh = f.hash) == MOVED)              tab = helpTransfer(tab, f);          else {              V oldVal = null;              // 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突              synchronized (f) {                  if (tabAt(tab, i) == f) {                      if (fh >= 0) {                          binCount = 1;                          for (Node<K,V> e = f;; ++binCount) {                              K ek;                              // 如果在链表中找到值为key的节点e,直接设置e.val = value即可。                              if (e.hash == hash &&                                  ((ek = e.key) == key ||                                   (ek != null && key.equals(ek)))) {                                  oldVal = e.val;                                  if (!onlyIfAbsent)                                      e.val = value;                                  break;                              }                              // 如果没有找到值为key的节点,直接新建Node并加入链表即可。                              Node<K,V> pred = e;                              if ((e = e.next) == null) {                                  pred.next = new Node<K,V>(hash, key,                                                            value, null);                                  break;                              }                          }                      }                      // 如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作。                      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;                          }                      }                  }              }              if (binCount != 0) {                  // 如果节点数>=8,那么转换链表结构为红黑树结构。                  if (binCount >= TREEIFY_THRESHOLD)                      treeifyBin(tab, i);                  if (oldVal != null)                      return oldVal;                  break;              }          }      }      // 计数增加1,有可能触发transfer操作(扩容)。      addCount(1L, binCount);      return null;  }  


0 0