hashMap源码分析hashtableConCurrentHashMap区别
Posted 码道小鑫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了hashMap源码分析hashtableConCurrentHashMap区别相关的知识,希望对你有一定的参考价值。
当前浏览器不支持播放音乐或语音,请在微信或其他浏览器中播放
1、hashMap的存取原理
(1).什么是hashMap,有什么特点?
hashMap是基于哈希表的Map接口的非同步实现,允许null键和null值,不保证映射顺序,是线程不安全的,底层数据结构是数组和链表的结合。
主要有2个重要参数:数组容量(capacity)和负载因子(Load factor)
Capacity是数组的容量,Load factor是数组的承载能力,当数组元素个数大于等于capacity*(Load factor)时将会resize扩容。一把为原来的2倍
(2)hashMap是怎样存数据的?
hashMap通过底层的put()方法对数据进行存贮。hashMap存贮的都是K-V值对,对于要存贮键值对,首先判断key是否为null,如果是则存放0,否则计算该键的hash值,通过hash值计算该数据应该在数组中存放的索引位置index。如果发现hash冲突,即这个数据的键的hash值计算出来的index位置已经有元素了,就需要对该索引位置的链表进行遍历,如果发现链表中存在一样的数据,利用equals方法对链表中的key一样,则把此时的元素值V赋值给此时对应的链表中有相同key的V,但要保证key的唯一性。如果不相等则把该数据添加到链表的头结点。在jdk8中,对于链表中的K-V键值对元素,如果数量超过了一定的值(一般为8)时,就需要把链表扩容为红黑树,这样就使得链表的查询的复杂度O(1)+O(N)变为了O(log(n))+O(1),提升了数据的查询性能。对于数组来说,如果数组中存放的位置不够用,超过了capacity*(Load factor)时就要扩容,让原始数组的容量乘以2。
代码如下:
public V put(K key, V value) { 2. // HashMap 允许存放null 键和null 值。 3. // 当key 为null 时,调用putForNullKey 方法,将value 放置在数组第一个位置。 4. if (key == null) 5. return putForNullKey(value); 6. // 根据key 的keyCode 重新计算hash 值。 7. int hash = hash(key.hashCode()); 8. // 搜索指定hash 值在对应table 中的索引。 9. int i = indexFor(hash, table.length); 10. // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 11. for (Entry<K,V> e = table[i]; e != null; e = e.next) { 12. Object k; 13. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 14. V oldValue = e.value; 15. e.value = value; 16. e.recordAccess(this); 17. return oldValue; 18. } 19. } 20. // 如果i 索引处的Entry 为null,表明此处还没有Entry。 21. modCount++; 22. // 将key、value 添加到i 索引处。 23. addEntry(hash, key, value, i); 24. return null; 25. } |
(3)hashMap时怎样取数据的?
hashMap通过底层的get()方法来取数据数据。一般分为两步:
第一步:首先计算要取数据的key的hash值,并计算该hash值再数组的对应索引位置。
第二步:判断该索引位置是否为null,如果是则返回null,如果不是,判断该位置是以链表形式存放还是以树的形式存放,并用key.equals(k)与链表或者树中的键进行比较,发现相等则返回链表或树对应位置的V值。
如果是链表:则在链表中通过key.equals(k)查找,时间复杂度为o(n)
如果是红黑树:则在红黑树中通过key.equals(k)查找,时间复杂度为o(log(n));
代码如下:
public V get(Object key) { 2. if (key == null) 3. return getForNullKey(); 4. int hash = hash(key.hashCode()); 5. for (Entry<K,V> e = table[indexFor(hash, table.length)]; 6. e != null; 7. e = e.next) { 8. Object k; 9. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 10. return e.value; 11. } 12. return null; 13. } |
(4)、你知道hash的实现吗?为什么要这样实现?
Hash的计算方式是通过key的hashCode值低16位与key的hashCode值高16位进行异或,这样的好处是可以充分利用数组的索引位置,并降低hash冲突。
(h=key.hashCode()^(key.hashCode()>>>16)),这样可以保证在数组容量较小时,可以保证高低位都可以参与到hash的计算中来。
(5)、在计算索引时,为什么是(hash&(length-1)),为什么数组的容量要为2^n?
因为在计算索引时是通过对数组长度的取模来得到的,而利用hash与length-1进行取模时利用与运算是很快的,而且可以使得索引在数组中得到均匀的分布,增大数组索引的利用率。同时由于length都是2^n次方,所以在length-1后二进制低位都是1,那么利用hash进行与运算时就相当于进行了取模运算。默认初始数组长度length=16。
(6)、hashMap的扩容
由于数组的容量是固定的,而hashMap虽然在每个数组位置上可以以链表或者红黑树的形式存贮数据,但如果数据在链表或者红黑树中存贮的太多,会严重影响查询效率,所以为了提高数据的查询效率,需要对数组容量进行扩容,扩容之后需要重新计算原来数组中的元素的索引位置。当数据在数组中存放的个数超过capacity*(Load factor)时,则需要进行2倍的扩容。其中Load factor=0.75,这是一种均衡的取法。在jdk8中如果扩容后hash值计算后新增位为0则表示原来数据位置不变,如果新计算后hash的新增位为1,则此时该数据在数组的索引位置为:原来的索引位置+原来数组的容量。
这样设计好处是重新计算的hash,新增的1bit是0还是1都是随机的,所以在重新计算数据的hash和索引后,对于原来的有冲突的数据又重新均匀的分配到其他索引位置上了。
Java集合小抄:
(1)以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
(2)插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。
(3)JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。
(4)在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。
(5)取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。
(6)iterator()时顺着哈希桶数组来遍历,看起来是个乱序
2、hashMap和hashtable的区别
(1)、hashMap和hashtable一样底层数据结构都是hash表,存贮的都是键值对数据,实现了map接口。
(2)、hashMap是线程不安全的,没有synchronized的同步,但运行效率高适合单线程,允许null,键和null值。
(3)Hashtable是线程安全的,有synchronized同步,但运行效率低,适合多线程,不允许存放null键和null值。
(4)Hashmap的不保证存储顺序,尤其不保证数据存贮顺序不随时间而改变。
(5)另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
3、HashTable和ConCurrentHashMap的区别?
ConCurrentHashMap是线程安全的hashmap,hashtable是synchronized同步的,每次对数据进行put或者get时都需要获取同步锁,锁的整个map,需要迭代很长时间才能释放锁,这样其它线程才能获取到锁执行程序,这样极大的降低了数据的存取速率,而ConCurrentHashMap是lock锁,将map分为很多段segment,对每个segment加锁,提高了锁的粒度,对存取数据时只对segment的进行操作就好,其它线程照样可以对其它数据进行操作存取,提高了数据存取效率。同时ConCurrentHashMap不允许null键和null值。
以上是关于hashMap源码分析hashtableConCurrentHashMap区别的主要内容,如果未能解决你的问题,请参考以下文章