Java8对HashMap和ConcurrentHashMap的改进

Java7 HashMap

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

 

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

Java8 HashMap

数组+链表+红黑树

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

  • (a)当某个bucket对应的链表元素个数>8
  • (b)HashMap总大小(总元素个数)>64
  • 如果红黑树中的元素个数<6就将其转化成链表

新结构如图:

 

扩容:

  • java7 插入在链表头,Java8 插入在链表尾

以下来自敖丙大神。。。

单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。一旦几个线程都调整完成,就可能出现环形链表

使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

  • java7 每个元素重新计算hash值,java 8  新 bucket=旧 bucket + 数组长度

优点

  • 除了插入操作以外,所有操作都加快了。原有链表的插入是在表头,就是bucket的位置。而新的插入,要进行红黑树的插入,时间复杂度为O(logn)(n是该bucket对应的元素个数)
  • 查找、删除操作红黑树都是O(logn), 而链表是O(n),所以其他的操作都是红黑树优先于链表,也就是Java8更快。
  • rehash操作:对于原链表,rehash需要对每个元素重新计算,但是采用红黑树中的元素,新 bucket= 旧bucket+原数组长度
  • Java8 链表的插入是在链表尾部,可以在扩容时保证链表元素原来的顺序,不会出现循环链表。

比如图中黄线标记的元素,它的新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所不具有的。

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采用了不同的方法

 


内存方面

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 字节。

 

ConcurrentHashMap

Java7   段+表+链表的形式

 

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

  1.  

Java8  ConcurrentHashMap:

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

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值