1.HashMap相关
几个重要参数:
-
容量,默认为16
-
负载因子,默认为0.75
-
扩容极限(暂不十分了解)
说明:当我们不指定任何参数创建HashMap时,就会创建一个容量为16,负载因子为0.75的HashMap,当HashMap中实际的元素个数大于等于16*0.75=12时,会触发HashMap的resize操作,HashMap的容量会自动扩展一倍。负载因子0.75被证明是性能比较好的取值,通常不会修改,那么只有初始同理会导致频繁扩容的行为,这是非常耗费资源的操作,所以如果能够事先估计出容器所需要存储的容量,就在初始化时修改默认值,即new HashMap(int initialCapacity)。
几个重要函数:hashCode() 和 equal()
- hashCode(),计算key值对应的哈希值,然后确定存储的数组索引;
- equal(),确定存储索引后,在索引处单向链表上遍历比较key值,看是否已经存储。
说明:这两个函数对实现HashMap的精确性和正确性至关重要jdk1.8 HashMap的底层实现:Entry数组 + 链表 + 红黑树。
jdk1.8 HashMap的底层实现:Entry数组 + 链表 + 红黑树。
首先,在HashMap类中有一个Entry的内部类,这个Entry类包含了key-value作为实例变量。没当向HashMap中存放k-v对时,都会为其实例化一个Entry对象,则个Entry对象就会存储Entry数组中。而Entry在数组中的具体位置,会根据key的HashCode()方法计算出的哈希值来决定。如果哈希算法设计的足够好,是不会发生碰撞冲突的,但实际中肯定没有这么理想,所以在每个索引处,会有一个单向链表,来存储相同索引的Entry对象。
然后,当调用put方法向哈希表中存储键值对时,首先计算key的hashcode,定位到合适的数组索引,然后在该索引上的单向链表进行遍历,用equals函数比较key是否存在。如果存在,则新的value值覆盖原来的value值;如果不存在,则将新的Entry对象插入到链表头部。且当链表长度大于某个值(可能是8)时,链表会变为红黑树,链表的查询效率为O(n),红黑树的查询效率为O(lgn),可见后者的效率更高。
最后,当需要取出一个Entry对象时,也是根据key的hashCode值来找到其存储位置,直接取出该Entry。
jdk1.7 HashMap的resize在多线程环境下,可能会产生条件竞争和死循环。
如果两个线程都发现HashMap需要重新调整大小,那么它们会同时试着去调整大小。在调整大小时,存储在链表中的元素的次序会反过来,因为在放入新的位置时,HashMap会将Entry对象不断的插入链表的头部。插入头部也主要是为了防止尾部遍历,否则这对key的HashCode相同的Entry每次添加还要定位到尾节点。如果条件竞争发送了,可能会出现环形链表,之后当我们get(key)操作时,就有可能发生死循环。
此外,虽然HashTable使用synchronized来保证线程安全,但是它会锁住整个哈希表,在线程竞争激烈的情况下,效率非常低,所以并不在多线程中经常使用HashTable。
思考:
HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。
HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。
HashMap,HashTable,ConcurrentHashMap的区别。
极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。