HashMap原理
hashMap内部包含了一个Entry类型的数组table
transient Entry[] table;
table数组中每个索引位置(可以将每个索引位置看成是一个桶bucket)存储着一条链表或者一棵红黑树。hashMap通过哈希算法计算出key对应的索引位置,不同的key计算出来的索引位置有可能出现冲突,拉链法和线性探测法可以解决位置冲突问题,hashMap采用的是拉链法。
拉链法的大致思路:使用哈希算法计算出索引位置,如果该索引位置对应的链表为空,则直接把键值对作为链表头节点,如果不为空(说明发生了碰撞),则遍历链表看是否有相同的key,如果有则替换掉该key对应的value值。如果链表中没有相同的key,则将该Entry节点插入到原链表的头部(头插法)。
hashCode的计算
key.hashCode()是key自带的hashCode函数,返回一个32位二进制值。
- 调用
key.hashCode()
- hashCode值高16位异或低16位(右移16位然后与原先的hashCode值异或,即自己的高半区与低半区做异或,这样混合后的低位掺杂了高位的部分信息):
return (key==null)?0:h=key.hashCode()^h>>>16;
- 取模运算:
h&(length-1)
其中length为table数组长度。h&(length-1) 等价于h%length
Java1.8中HashMap中扩容
首先在HashMap中规定数组的长度一定为2次幂,扩展后的数组长度为原先的2倍(保证新数组长度也是2次幂)。
假设原数组的长度old capacity为16,扩容后new capacity为32=2*16:
old capacity :00010000
new capacity :00100000
对于一个key:
- 如果它的哈希值在第5位为0,则取模后的索引位置与原先一致
- 如果为1,那么取模后的位置为原来的“ 索引位置+old capacity"
这样我们就无需重新计算hash了,只需要查看某个bit位置为1还是0就可以知道其新的索引位置。
HashMap中链表和红黑树之间的转换
当同一个数组位置下的链表长度大于8时,为了提高查询和修改效率,会将链表转换为红黑树。而当红黑树节点数量小于6时,会将红黑树转换为链表结构。
HashMap中插入空值
HashMap中插入key=null的Entry节点时会调用putForNullKey方法直接去遍历table[0]位置的链表,如果已经存在key为null的Entry节点,则将其value替换掉,否则调用addEntry方法头插一个节点到table[0]位置。
ps:HashTable不能插入key=null的键值对。
HashMap为什么线程不安全
- 两个线程对同一个table数组索引位置添加节点,其中一个线程的写入操作会被另外一个线程的写入操作覆盖,造成写入丢失。
- 两个线程对同一个table数组索引位置删除节点,其中一个线程的删除操作会被另外一个线程的删除操作覆盖,造成删除丢失。
- 两个线程同时开始resize,如果线程A先完成resize的情况下,线程B在resize的过程中很有可能会引用到线程A执行resize后的table,造成错误。
ConCurrentHashMap如何实现线程安全
java1.7使用Segment分段锁机制实现并发安全,每个Segment维护多个bucket,即多个索引位置,一个segment锁只能同时被一个线程拥有。默认采用16个分段锁,即并发度为16。
java1.8采用CAS+synchronized来保证并发安全。
举例put()方法:
首选根据key计算出对应的hashcode,然后得到其table对应的索引位置。
假设f为当前key定位到的索引位置的头节点node,如果头节点为null(即链表为空),则利用CAS操作尝试写入,失败则进行自旋直到成功。
如果头节点不为null(说明链表不为空),此时需要使用synchronized关键字将链表的头节点锁定,防止其他线程同时对该链表进行修改。然后再遍历链表进行对应的写入操作。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//table数组没有初始化,就进行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//头节点为null则尝试使用cas写入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//是否需要进行resize
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {//头节点f不为null,锁住头节点(即锁住该链表)
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//是否需要进行红黑树转换
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
HashMap与HashTable的区别
- hashMap不是线程安全的,hashTable是线程安全的,
- 通过使用synchronized关键字来修饰所有的修改方法来实现线程安全。
- hashMap允许null值,而hashTable不允许null值(key和value都不可以)
- hashMap基于AbstractMap类,而hashTable基于Dictionary类
ConCurrentHashMap与HashTable区别
- 两者都是线程安全的,但是hashTable锁住的是整个map,效率低下。而ConcurrentHashMap使用的是cas+synchronized机制,不会锁定整个map,而是锁定table数组位置对应的链表。
- 一般不要使用hashTable,推荐使用ConCurrentHashMap。