HashMap中的resize问题

在jdk1.8中,hashMap的resize()函数做了相应的调整,尤其是对于在buckets的链表中,官方给出的该resize()函数主要在两种情况下使用:

  1. 初始化的时候
  2. 将哈希表扩容成之前的两倍时

下面首先看初始化时,实际的resize()函数做了哪些工作:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = 0 ;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 初始化传过来时候threshold为0
    newCap = DEFAULT_INITIAL_CAPACITY; // 16
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75*16=12

    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    return newTab;
}

从代码逻辑来看,初始化时resize()就是将 threshold=12,以及 table=new Node[16];

当哈希表需要扩容时:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap =  oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 下面这个if中主要是将 newCap=2*oldCap,newThr=2*oldThr
    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; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        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; // threshold=2*oldThreshold
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 将原来哈希表中的数据移到新的table里面
    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)                            // 此处的bucket只有一个元素,后边没接链表
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)                // 此处的bucket下为红黑树
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order                       // 此处bucket后接了链表
                    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;
}

可以看到,在对哈希表扩容后,resize()还做了一个最重要的工作,就是将原来table中的数据转移到新的table当中,大概有三种情况:

  1. oldTable[i]处只有一个元素e: 转到新的newTable中,位置应该为:newTable[e.hash&(newCap -1)]
  2. oldTable[i]处为树节点:之后在讨论
  3. oldTable[i]后接了一个链表:重点讨论

如下图:

在扩容时,newTable的容量变为原来的两倍,要把链表上的元素迁移到newTable上,需要按照 e.hash & (newCap -1) 计算出该元素在newTable上的哪个bucket里面。

由于hashMap的容量总是2的倍数,那么在计算新的索引位置时,与操作的结果就是将原来元素的hash值再高一位与1进行&操作,为0的结果为0,为1的结果为1. 当hash值高一位为0时,在newTable的索引与之前的一样;当hash值高一位为1时,newTable的索引相当于oldTable上的索引+ oldTable的长度。可以参照上面的22和38.

所以,官方给出jdk1.8中resize()的注释是:经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

为了判断原来hash值高一位是0还是1,jdk1.8直接(e.hash & oldCap)来判断,结果等于0原来高一位就是0,否则就是1。这样正好也就形成了两个新的链表,loHead-->loTail还在原来的bucket中,hiHead--->hiTail处于新的位置。新的链表的顺序和原来的一致。

jdk1.7 resize

在jdk1.7中,resize()方法与1.8大体类似,也是扩容,具体:

void resize(int newCapacity) {   //传入新的容量
    Entry[] oldTable = table;    //引用扩容前的Entry数组
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了
        threshold = Integer.MAX_VALUE;      // 修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
        return;
    }

    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
    transfer(newTable);                         //!!将数据转移到新的Entry数组里
    table = newTable;                           //HashMap的table属性引用新的Entry数组
    threshold = (int) (newCapacity * loadFactor);//修改阈值
}

其中transfer()函数是将oldTable元素转移到newTable中,具体实现:

void transfer(Entry[] newTable) {
    Entry[] src = table;                   //src引用了旧的Entry数组
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素
        if (e != null) {
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
            do {
                Entry<K, V> next = e.next;
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
                e.next = newTable[i];                  //标记[1]
                newTable[i] = e;                       //将元素放在数组上
                e = next;                              //访问下一个Entry链上的元素
            } while (e != null);
        }
    }
}

比较难理解的是e.next=newTable[i]; newTable[i] = e; e=next;几个操作:

i是e元素在newTable中的索引位置,这几个操作相当于是把e这个元素放在newTable[i]位置上,原来newTable[i]处有元素的话,按照链表往后排,也就是相当于 新元素插在链表的头位置

resize()造成多线程死循环

在jdk1.7版本当中,因为e.next与e的问题,所以在多线程中由于并发问题形成有环链表,在get查找时可能会形成死循环。

其形成过程如下:

  1. 存在一个hashmap如图左侧,在索引为3的位置有链表a-->b,当扩容时resize为右图所示,恰好a,b还处于新的索引位置7, 不过按照1.7顺序正好颠倒;

  2. 多线程时,假设线程1和线程2同时执行,都会创建新的数组,但是线程2执行到 next=e.next时,cpu切换到线程1上,如下图,此时e指向a,next指向b;

  3. 线程1上述操作正常的transfer了oldTable,但是还没有执行table=newTable,此时线程1切换到线程2,状态如下:

  4. 线程2继续执行之后的逻辑:
    Entry<K, V> next = e.next;
    int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
    e.next = newTable[i];                  //标记[1]
    newTable[i] = e;                       //将元素放在数组上
    e = next; 
    • 将a指向索引7,e指向元素b,next指向b的next即元素a,如图4-1
    • 因为e不为null,继续执行循环体,在线程2中同样形成b-->a的链表,同时e指向next即元素a,如图4-2
    • 再次执行循环体,next为a.next即为null,然后将a.next指向newTable[i],此时即为b,形成a,b相互引用,如图4-3
    • 因为e指向next为null,transfer结束

由上可见,jdk1.7中多线程能形成环状,除了没有相应同步机制的原因,主要因为有一个倒序(每次把元素指向bucket首位)的问题。在jdk1.8中多线程不会出现上述resize后成为有环链表的问题,但是多线程的本质问题还是存在。

原文地址:https://www.cnblogs.com/wongdongd/p/12386897.html

时间: 2024-10-08 20:41:42

HashMap中的resize问题的相关文章

jdk1.8 HashMap的扩容resize()方法详解

/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or mo

关于hashMap中 计算hashCode的逻辑推理(二)

hashMap中,为了使元素在数组中尽量均匀的分布,所以使用取模的算法来决定元素的位置.如下: 1 //方法一: 2 static final int hash(Object key){//jdk1.8 3 int h; 4 return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); 5 } 6 //方法二: 7 static int indexFor(int h,int length){//低版本的源码 8 return h

HashMap中的keySet()和entrySet()

1.基本概述 Set<Map.Entry<K,V>> entrySet()  返回此映射中包含的映射关系的 set 视图. Set<K>              keySet()      返回此映射中包含的键的 set 视图. 2.效率分析 对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value.而entryset只是遍历了第一次,他把key和value都放到了entry中,所以就快了. 3.使用实例 pa

Java学习笔记--HashMap中使用object做key的问题【转载】

在HashMap中,如果需要使用多个属性组合作为key,可以将这几个属性组合成一个对象作为key.但是存在的问题是,要做get时,往往没办法保存当初put操作时的key object的reference,此时,需要让key object覆盖如下hashCode()和equals(Object obj)的实现.sample code如下: public class TestKeyObject { private long id; private int type; public TestKeyOb

vector中的resize与 reserve

void reserve (size_type n); reserver函数用来给vector预分配存储区大小,即capacity的值 ,但是没有给这段内存进行初始化.reserve 的参数n是推荐预分配内存的大小,实际分配的可能等于或大于这个值,即n大于capacity的值,就会reallocate内存 capacity的值会大于或者等于n .这样,当调用push_back函数使得size 超过原来的默认分配的capacity值时 避免了内存重分配开销. 需要注意的是:reserve 函数分配

List&lt;HashMap&lt;String,String&gt;&gt; list, 根据hashmap中的某个键的值排序

//可以使用Collections.sort(List list, Comparator c)来实现 这里举例hashmap中存的一个时间的键值,按照时间的值来排序 //先写个类实现Comparator,并重写compare(Object o1, Object o2)方法,在方法中自定义比较逻辑 public class MyComparator implements Comparator { @Override     public int compare(Object o1, Object 

如果两个对象具有相同的哈希码,但是不相等的,它们可以在HashMap中同时存在吗?

----答案是 可以 原因: 在hashmap中,由于key是不可以重复的,他在判断key是不是重复的时候就判断了hashcode这个方法,而且也用到了equals方法. 这里不可以重复是说equals和hashcode只要有一个不等就可以了. 一.当我们向一个set.HashMap.HashSet.HashTable集合中添加某个元素,集合会首先调用该对象的hashCode方法, 这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存.若该处已经有元素存在,就调用equals方法来匹

java自定义类型 作为HashMap中的Key值 (Pair&lt;V,K&gt;为例)

由于是自定义类型,所以HashMap中的equals()函数和hashCode()函数都需要自定义覆盖. 不然内容相同的对象对应的hashCode会不同,无法发挥算法的正常功能,覆盖equals函数,应该就相当于c++重载==运算符来保证能判断是否相等.只不过java没有自定义重载运算符这个功能的,需要进行函数覆盖. equals的函数原型是 boolean equals(Object o);注意括号内.hashCode的函数原型就是int hashCode(); 先看一段代码: import

HashMap中使用自定义类作为Key时,为何要重写HashCode和Equals方法

之前一直不是很理解为什么要重写HashCode和Equals方法,才只能作为键值存储在HashMap中.通过下文,可以一探究竟. 首先,如果我们直接用以下的Person类作为键,存入HashMap中,会发生发生什么情况呢? public class Person { private String id; public Person(String id) { this.id = id; } } import java.util.HashMap; public class Main { public