基于jdk1.7的HashMap

jdk1.7的HashMap底层有数组加链表实现。

  1. 为什么HashMap的key对象重写equals方法需同时重写hashCode方法

有人觉得get操作和put操作在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null。
如下面的例子:

package com.dalingjia.test;

import java.util.HashMap;

public class MyTest {

    private static class Person {
        int idCard;
        String name;

        public Person(int idCard, String name) {
            this.idCard = idCard;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Person person = (Person) o;
            //两个对象是否等值,通过idCard来确定
            return this.idCard == person.idCard && this.name.equals(person.name);
        }
    }

    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<Person, String>();
        Person person = new Person(1234, "乔峰");
        System.out.println(person.hashCode());
        Person person1 = new Person(1234, "乔峰");
        System.out.println(person1.hashCode());
        //put到hashmap中去
        map.put(person, "天龙八部");
        //get取出,从逻辑上讲应该能输出“天龙八部”
        System.out.println("结果:" + map.get(person));
        System.out.println("结果:" + map.get(person1));
    }
}

输出结果如下:
在这里插入图片描述
尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)–>hash–>indexFor–>最终索引位置 ,而通过key取出value的时候 key(hashcode2)–>hash–>indexFor–>最终索引位置,由于两个对象的hashcode()方法返回值不同,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)

代码如下:看if判断

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];  e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;
        }
        return null;
}

所以,某个对象在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)。

  1. HashMap的默认初始长度是多少?为什么这么规定?
    HashMap的默认初始长度为16,并且每次自动扩展或是手动初始化时,长度必须是2的幂。之所以选择16,是为了服务于从key映射到index的Hash算法。取模运算效率较低,为了实现高效率的hash算法,HashMap的发明者采用了位运算的方式。源码如下:(length是HashMap的长度)
    static int indexFor(int hash, int length) {
        return hash & (length-1);
    }

我们以值为“book"的key来演示:

  • 计算book的hashcode,结果为十进制的3029737,转换为二进制为101110001110101110 1001。
  • 假定HashMap的长度默认是16,计算length-1的15,二进制为1111.
  • 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所有index=9。可以说hash算法最终得到的index结果,完全取决于key的hashcode值的最后几位。

假设HashMap的长度为10,重复上述步骤:
在这里插入图片描述
单独看这个结果,没什么问题,结果仍然是hashcode的最后几位,我们再试一个新的hashcode:
在这里插入图片描述
再试一个:
在这里插入图片描述
这样运算结果都是1001,导致有些index结果出现的几率变大,而有些index结果永远不会出现(比如0111),这样显然不符合Hash算法均匀分布的原则。反观长度为16或其他2的次幂,length-1的值是所有二进制位全为1,这样index的结果等同于hashcode后几位的值。只要输入的hashcode值本身是均匀的,那么hash算法的结果就是均匀的。

  1. 高并发情况下,为什么HashMap可能会出现死锁?
    首先看一下1.7的扩容机制:
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

很明显,当HashMap中元素个数大于等于阀值,且数组对应的下标元素不为null时,就会考虑扩容;但是如果此时的HashMap的长度为MAXIMUM_CAPACITY(1<<30)时,会将阀值设置为Integer.MAX_VALUE,不考虑进行扩容,直接插入元素。否则就进行扩容,并将旧数组中的元素rehash到新的数组中。代码如下:

//扩容操作
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

真正的rehash操作:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //1,遍历旧数组
        for (Entry<K,V> e : table) {
        	//2,遍历旧数组某个元素对应的链表
            while(null != e) {
            	//3,链表的下一个结点
                Entry<K,V> next = e.next;
                //4,重新hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //5,旧元素对应新数组的下标
                int i = indexFor(e.hash, newCapacity);
                //6,旧元素结点做头结点插入新数组
                e.next = newTable[i];
                newTable[i] = e;
                //7,继续遍历链表的下一个结点
                e = next;
            }
        }
    }

现有A和B两个线程,对长度为4的HashMap做put操作,虽然size=3,大于等于阀值(4*0.75),但是数组对应的下标元素为null,不需要扩容,直接插入。这个问题在上面有说明。
在这里插入图片描述
在这里插入图片描述
此时达到resize的两个条件,两个线程各自进行resize的第一步操作:扩容
在这里插入图片描述
在这里插入图片描述
假如线程B遍历到Entry3对象,刚执行完红框中的代码,线程就被挂起,对于线程B来说:e = Entry3 , next = Entry2 ; 这时线程A畅通无阻地进行rehash操作,完成后,结果如下(图中e和next代表线程B的两个引用):
在这里插入图片描述
接下来线程B恢复,继续执行属于他自己的rehash操作,线程b刚才的状态是:e = Entry3 , next = Entry2 ;
在这里插入图片描述
当执行晚上面这一行代码,显然i = 3;应为线程A对于Entry3的hash结果也是3。
在这里插入图片描述
我们继续执行到这两行代码,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2,此时 e = Entry2 , next = Entry2 ;
在这里插入图片描述
接着又一轮循环,执行到下面红框中的代码:
在这里插入图片描述
e = Entry2 , next = Entry3 ;
在这里插入图片描述
接下来执行下面的三行代码,用头插法把Entry2插入到线程B的数组的头结点:
在这里插入图片描述
整体情况如下:
在这里插入图片描述
第三次循环开始,又执行到红框中的代码:
在这里插入图片描述
此时:e = Entry3 , next = Entry3.next = null ;
最后我们执行下面这行代码:
在这里插入图片描述
newTable[i] = Entry2 ;
e = Entry3 ;
Entry2.next = Entry3 ;
Entry3.next = Entry2 ;
即Entry3的引用指向Entry2, Entry2的引用指向Entry3,链表出现环形!如下图:
在这里插入图片描述
此时问题还没有直接产生,当调用get方法时,查找一个不存在的key,而这个key刚好定位到i = 3的位置,由于位置3带有环形链表,所以程序进入死循环!所以高并发下的HashMap1.7会出现并发问题。
总结:hashMap1.7不是线程安全的,在多线程并发的情况下可能会形成环形链表,让下一次读操作出现死循环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值