绕树三匝,何枝可依。——曹操《短歌行》
HashMap是什么?
HashMap是基于哈希表实现了Map接口的一个类,它不是线程安全的,即不是同步的。是一种存储键值对的集合,它允许null值和null键,它可以根据给定的key通过哈希函数计算得到存储的位置。
HashMap默认初始容量一般是16,默认负载因子是0.75,当散列表中的数目超过容量的75%时,散列表将会重建为原来的两倍,通过包含两个步骤,即重新开辟空间和重新计算散列。
为什么默认初始容量是16,默认负载因子是0.75?
默认初始容量16更加符合hash算法的均匀分布原则,倘若是其他则会更加容易引起哈希冲突。负载因子是综合考虑了时间与空间成本的良好折中,更高会降低空间成本,但会增加查找开销(包括put和get)。
HashMap的put与get方法
put
1 | public V put(K key, V value) {return putVal(hash(key), key, value, false, true);} |
当我们调用put方法,传入key和value时,该类又调用了putVal方法,并传入相关参数,其中hash(key)方法源码是:
1 | static final int hash(Object key) { |
可以看到,hash方法是计算我们传入put方法key的哈希码,若key为null则哈希值为0,否则获取key的哈希码并将它与它无符号右移16位后的值做异或运算(无符号右移:高位用0补,右移;异或运算:相异为1,相同为0)。即首先需要计算key的哈希值。
putVal的源码:
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
get
get源码:
1 | public V get(Object key) { |
可以看到,我们是通过给出key而获取value值,首先也是与put方法一样获取key的哈希码,然后调用getNode方法获取对应的Node对象,判断对象是否为空,若为空则返回空,否则返回我们所需要的value值。接下来看看getNode方法的源码,瞅瞅它是如何通过key获取到与key对应的node对象的。
1 | final Node<K,V> getNode(int hash, Object key) { |
总结与体会
HashMap的key与value是通过“中介”node去存储相应的key和value的,存储方式通过散列函数(n-1) & key,这样得到的地址减少了地址冲突,有益于存储。再有就是对相同额的散列地址是通过链表额的形式存储,通过指针next去加或找相应的node,也存在通过红黑树作为相同散列地址存储方式的(但是我对这个不熟悉,因此没有深入的分析,记住教训,即将学习它)。emmm。。。还有很多。希望自己多多读源码,找到感觉,再接再厉!
第一次读源码,读得很吃力,有很多不足之处,请多多指教!阿里嘎多^_^
HashMap高并发下引起的死锁
前面有聊到HashMap的默认负载因子是0.75,这是因为HashMap的容量达到这个阈值会增加哈希冲突,于是就有了扩容rehash这个动作。扩容是扩充到原来的两倍,扩充动作是两步,首先扩充entry数组到原来的两倍,然后再重新hash原来的map到新的entry,此时需要重新计算散列地址,因为长度变了。由于HashMap并非线程安全的,于是当重新hash时,便可能会引起线程死锁。
具体来说是:
请看大佬^_*^,我的理解是当我们重新计算哈希时,由于相同位置的node链表形成了循环链表,当查找该位置的node值为null时,就会不断查找,于是形成了死锁。
对于该死锁,大佬的解决方案是使用另一个集合类ConcurrentHashMap,该类兼顾了线程安全和性能。通过学习该文章,我的理解是jdk1.7是在插入时通过该类继承ReentranLock类而使用类方法加锁的方式去对插入的node进行同步控制,而jdk1.8是对代码块使用synchronized关键字进行同步控制。
关于ConcurrentHashMap的深入研究被挂起,等到进一步源码学习^%_*^