目录
3、hashMap、hashTable、ConcurrentHashMap对比?
hashMap
1、haspmap扩容机制
1、什么是扩容机制?
当数据容量超过当前最大容量*loadfactor(loadfactor表示扩容因子)时,容量自动扩大2倍,并将当前的数据重新放入新的hashmap中。所以初始的定义大小为2^n的大小最佳。
2、什么是resize?
resize就是重新计算容量;当我们不断的向HashMap对象里不停的添加元素时,HashMap对象内部的数组就会出现无法装载更多的元素,这是对象就需要扩大数组的长度,以便能装入更多的元素;当然Java里的数组是无法自动扩容的,方法的本质是使用一个新的数组代替已有的容量小的数组;就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
3、什么时候需要resize()?
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容。
2、hashmap为啥会有线程安全问题?
本质原因:put()、addEntry()、resize()方法不是同步的,导致多线程并发的时候会出错,造成数据不一致、覆盖、死循环的错误。比如,JDK7中HashMap并发put会造成循环链表,导致get时出现死循环此问题在JDK8中已经解决。
1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、HashMap的get操作在并发时,可能因为resize而引起死循环(cpu100%)
具体需要分析源码,参考资料 https://juejin.im/post/5ba457a25188255c7b168023
3、hashMap、hashTable、ConcurrentHashMap对比?
主要特点 | 线程是否安全 | 原因 | |
hashMap | HashMap内部是保存链表头节点的桶数组,根据key的hashCode值来保存value,需要注意的是,HashMap不保证遍历的顺序和插入的顺序是一致的。 | 否 | |
hashTable | 是 | HashTable类是线程安全的,它使用synchronize来做线程安全,全局只有一把锁,在线程竞争比较激烈的情况下hashtable的效率是比较低下的 | |
ConcurrentHashMap | 是 | ConcurrentHashMap使用了分段锁技术来提高了并发度,不在同一段的数据互相不影响,多个线程对多个不同的段的操作是不会相互影响的。 | |
hashMap 子类--LinkedHashMap | 与HashMap的区别在于LinkedHashMap保存了记录插入的顺序。 | ||
TreeMap | TreeMap实现了SortedMap接口,TreeMap有能力对插入的记录根据key排序,默认按照升序排序,也可以自定义比较强,在使用TreeMap的时候,key需实现Comparable。 |
4、JDK1.7 和1.8 关于hashMap尾插
HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
区别 | |
JDK1.7 | HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。 |
JDK1.8 | 当单链表中元素个数超过8个时,会进而转化为红黑树存储,巧妙地将遍历元素时时间复杂度从O(n)降低到了O(logn)) |
JDK1.7 头插法 | 在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。 |
JDK1.8 尾插法 | 在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。(链表成环问题解决!) |