Map相关问题总结
HashMap
1.7
数组+链表
头插
1.8
数组+链表+红黑树
尾插
1.为什么扩容长度是2的n次方?:方便位运算,均匀分布
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
整个过程本质上就是三步:
拿到 key 的 hashCode 值
将 hashCode 的高位参与运算,重新计算 hash 值
将计算出来的 hash 值与 (table.length - 1) 进行 & 运算
当 table 长度为 16 时,table.length - 1 = 15 ,用二进制来看,此时低 4 位全是 1,高 28 位全是 0,与 0 进行 & 运算必然为 0,因此此时 hashCode 与 “table.length - 1” 的 & 运算结果只取决于 hashCode 的低 4 位,在这种情况下,hashCode 的高 28 位就没有任何作用,并且由于 hash 结果只取决于 hashCode 的低 4 位,hash 冲突的概率也会增加。因此,在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。
2.java1.7的特点,数组+链表,头插, java1.8 ,数组+链表+红黑树,尾插
//map的默认参数
// 默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表节点转换红黑树节点的阈值, 9个节点转
static final int TREEIFY_THRESHOLD = 8;
// 红黑树节点转换链表节点的阈值, 6个节点转
static final int UNTREEIFY_THRESHOLD = 6;
// 转红黑树时, table的最小长度
static final int MIN_TREEIFY_CAPACITY = 64;
// 链表节点, 继承自Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ... ...
}
// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//(n - 1) & hash取到table的下标,获取到root节点
if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
//判断hash
if (first.hash == hash &&
//判断key
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//头结点不匹配并且有next
if ((e = first.next) != null) {
//如果是红黑树遍历树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不是红黑树,遍历列表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//todo getTreeNode解读,待补充
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table为空时调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算下标并且该位置为null时,直接放到该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//key如果相等,后续只需替换value
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历插入链表,使用binCount表示链表的节点数
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD=8即当链表长大于8时需要调treeifyBin转换为tree
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果插入节点后节点数超过阈值,则调用resize方法进行扩容
//threshold=table.length * DEFAULT_LOAD_FACTOR,默认为16*0.75=12
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize 方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap,oldThr 分别左移1位,变为32和24
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化
newCap = DEFAULT_INITIAL_CAPACITY; //16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //12
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//初始化结束
if (oldTab != null) {
//扩容后遍历旧数组,放到新数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
//e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果是链表节点,则进行重hash分布
Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> next;
do {
next = e.next;
// 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
// 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
// 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
// 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
// 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1 如果 e 的 hash 值与老表的容量进行位与运算为 0,则说明 e 节点扩容后的索引位置跟老表的索引位置一样(见例子1详解),进行链表拼接操作:如果 loTail 为空,代表该节点为第一个节点,则将 loHead 赋值为该节点;否则将节点添加在 loTail 后面,并将 loTail 赋值为新增的节点。
2 如果 e 的 hash 值与老表的容量进行位与运算为 1,则说明 e 节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1详解),进行链表拼接操作:如果 hiTail 为空,代表该节点为第一个节点,则将 hiHead 赋值为该节点;否则将节点添加在 hiTail 后面,并将 hiTail 赋值为新增的节点。、
死循环问题
在 JDK 1.7,Java 语言在并发情况下使用 HashMap 造成 Race Condition,从而导致死循环。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//此处并发
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。
然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起。此时线程A中运行结果如下:
B线程执行完成
此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,
由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。后续循环会形成7,3的环。