上一篇文章中提到了ThreadLocalMap是使用开放地址法来解决冲突问题的,而咱们今天的主角HashMap是采用了链表法来处理冲突的,什么是链表法呢?java
在散列表中,每一个 “ 桶(bucket)” 或者 “ 槽(slot)” 会对应一条链表,全部散列值相同的元素咱们都放到相同槽位对应的链表中。node
jdk8和jdk7不同,jdk7中没有红黑树,数组中只挂载链表。而jdk8中在桶容量大于等于64且链表节点数大于等于8的时候转换为红黑树。当红黑树节点数量小于6时又会转换为链表。数组
但插入的时候,咱们只须要经过散列函数计算出对应的槽位,将其插入到对应链表或者红黑树便可。若是此时元素数量超过了必定值则会进行扩容,同时进行rehash.安全
经过散列函数计算出对应的槽,而后遍历链表或者删除bash
上一篇文章有提到过经过装载因子来断定空闲槽位还有多少,若是超过装载因子的值就会动态扩容,HashMap会扩容为原来的两倍大小(初始容量为16,即槽(数组)的大小为16)。可是不管负载因子和散列函数设得再合理,也避免不了链表过长的状况,一旦链表过长查找和删除元素就比较耗时,影响HashMap性能,因此JDK8中对其进行了优化,当链表长度大于等于8的时候将链表转换为红黑树,利用红黑树的特色(查找、插入、删除的时间复杂度最坏为O(logn)),能够提升HashMap的性能。当节点个数少于6个的时候,又会将红黑树转化为链表。由于在数据量较小的状况下,红黑树要维持平衡,比起链表来,性能上的优点并不明显,并且编码难度比链表要大上很多。数据结构
public HashMap(int initialCapacity, float loadFactor);
public HashMap(int initialCapacity);
public HashMap();
复制代码
HashMap的构造方法中能够分别指定初始化容量(bucket大小)以及负载因子,若是不指定默认值分别是16和0.75.它几个重要属性以下:多线程
// 初始化容量,必需要2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 须要从链表转换为红黑树时,链表节点的最小长度
static final int TREEIFY_THRESHOLD = 8;
// 转换为红黑树时数组的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// resize操做时,红黑树节点个数小于6则转换为链表。
static final int UNTREEIFY_THRESHOLD = 6;
// HashMap阈值,用于判断是否须要扩容(threshold = 容量*loadFactor)
int threshold;
// 负载因子
final float loadFactor;
// 链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
// 保存数据的数组
transient Node<K,V>[] table;
// 红黑树节点
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;
}
复制代码
上面的table就是存储数据的数组(能够叫作桶或者槽),数组挂载的是链表或者红黑树。值得一提的是构造HashMap的时候并无初始化数组容量,而是在第一次put元素的时候才进行初始化的。并发
int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
int index = hash & (tab.length-1);
复制代码
从上面能够看出,key为null是时候放到数组中的第一个位置的,咱们通常定位key应当存放在数组哪一个位置的时候通常是这样作的 key.hashCode() % tab.length
。可是当tab.length是2的n次幂的时候,就能够转换为 A % B = A & (B-1)
;因此 index = hash & (tab.length-1)
就能够理解了。app
这里是使用了除留余数法的理念来设计的,能够可能减小hash冲突 除留余数法 : 用关键字K除以某个不大于hash表长度m的数p,将所得余数做为hash表地址 好比x/8=x>>3,即把x右移3位,获得了x/8的商,被移掉的部分(后三位),则是x%8,也就是余数。函数
而对于hash值的运算为何是(h = key.hashCode()) ^ (h >>> 16)
呢?也就是为何要向右移16位呢?直接使用 key.hashCode() & (tab.length -1)
很差吗? 若是这样作,因为tab.length确定是远远小于hash值的,因此位运算的时候只有低位才参与运算,而高位毫无做为,会带来hash冲突的风险。
而hashcode自己是一个32位整形值,向右移位16位以后再进行异或运行计算出来的整形将具备高位和低位的性质,就能够获得一个很是随机的hash值,在经过除留余数法,获得的index就更低几率的减小了冲突。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 若是数组未初始化,则初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 若是当前节点未被插入数据(未碰撞),则直接new一个节点进行插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. 碰撞了,已存在相同的key,则进行覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 4. 碰撞后发现为树结构,则挂载在树上
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 5. 进行尾插入,若是链表节点数达到上线则转换为红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 6. 链表中碰撞了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 7. 用新value替换旧的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 8. 操做阈值则进行扩容
if (++size > threshold)
resize();
// 给LinkedHashMap实现
afterNodeInsertion(evict);
return null;
}
复制代码
简述下put的逻辑,它主要分为如下几个步骤:
resize()方法中,若是发现当前数组未初始化,则会初始化数组。若是已经初始化,则会将数组容量扩容为以前的两倍,同时进行rehash(将旧数组的数据移动到新的数组).JDK8的rehash过程颇有趣,相比JDK7作了很多优化,咱们来看下这里的rehash过程。
// 数组扩容为以前2倍大小的代码省略,这里主要分析rehash过程。
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 1. 若是旧数组中不存在碰撞,则直接移动到新数组的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 2. 若是存在碰撞,且节点类型是树节点,则进行树节点拆分(挂载到扩容后的数组中或者转为链表)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 3. 处理冲突是链表的状况,会保留原有节点的顺序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 4. 判断扩容后元素是否在原有的位置(这里很是巧妙,下面会分析)
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 5. 元素不是在原有位置
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 6. 将扩容后未改变index的元素复制到新数组
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 7. 将扩容后改变了index位置的元素复制到新数组
if (hiTail != null) {
hiTail.next = null;
// 8. index改变后,新的下标是j+oldCap,这里也很巧妙,下面会分析
newTab[j + oldCap] = hiHead;
}
}
}
}
}
复制代码
上面的代码中展示了整个rehash的过程,先遍历旧数组中的元素,接着作下面的事情
index=e.hash & (newCap - 1)
。(e.hash & oldCap) == 0
判断,oldCap表示扩容前数组的大小。j + oldCap
。其中第4点和第5点中将链表的元素分为两部分(do..while部分),一部分是rehash后index未改变的元素,一部分是index被改变的元素。分别用两个指针来指向头尾节点。
好比当oldCap=8时,1-->9-->17都挂载在tab[1]上,而扩容后,1-->17挂载在tab[1]上,9挂载在tab[9]上。
那么是如何肯定rehash后index是否被改变呢?改变以后的index又变成了多少呢?
这里的设计非常巧妙,还记得HashMap中数组大小是2的n次幂吗?当咱们计算索引位置的时候,使用的是 e.hash & (tab.length -1)。
这里咱们讨论数组大小从8扩容到16的过程。
tab.length -1 = 7 0 0 1 1 1
e.hashCode = x 0 x x x x
==============================
0 0 y y y
复制代码
能够发如今扩容前index的位置由hashCode的低三位来决定。那么扩容后呢?
tab.length -1 = 15 0 1 1 1 1
e.hashCode = x x x x x x
==============================
0 z y y y
复制代码
扩容后,index的位置由低四位来决定,而低三位和扩容前一致。也就是说扩容后index的位置是否改变是由高字节来决定的,也就是说咱们只须要将hashCode和高位进行运算便可获得index是否改变。
而恰好扩容以后的高位和oldCap的高位同样。如上面的15二进制是1111,而8的二进制是1000,他们的高位都是同样的。因此咱们经过e.hash & oldCap运算的结果便可判断index是否改变。
同理,若是扩容后index该变了。新的index和旧的index的值也是高位不一样,其新值恰好是 oldIndex + oldCap的值。因此当index改变后,新的index是 j + oldCap。
至此,resize方法结束,元素被插入到了该有的位置。
get()的方法就相对来讲要简单一些了,它最重要的就是找到key是存放在哪一个位置
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 首先(n-1) & hash肯定元素位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2. 判断第一个元素是不是咱们须要找的元素
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 3. 节点若是是树节点,则在红黑树中寻找元素
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
4. 在链表中寻找对应的节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
复制代码
remove方法寻找节点的过程和get()方法寻找节点的过程是同样的,这里咱们主要分析寻找到节点后是如何处理的
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 1. 删除树节点,删除时若是不平衡会从新移动节点位置
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 删除的节点是链表第一个节点,则直接将第二个节点赋值为第一个节点
else if (node == p)
tab[index] = node.next;
// 删除的节点是链表的中间节点,这里的p为node的prev节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
复制代码
remove方法中,最为复杂的部分应该是removeTreeNode部分,由于删除红黑树节点后,可能须要退化为链表节点,还可能因为不知足红黑树特色,须要移动节点位置。 代码也比较多,这里就不贴上来了。但也所以佐证了为何不所有使用红黑树来代替链表。
/** * Transfers all entries from current table to newTable. */
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
// B线程执行到这里以后就暂停了
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
// 会把元素放到链表头,因此扩容后数据会被倒置
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
复制代码
扩容时上面的代码容易致使死循环,是怎样致使的呢?假设有两个线程A和B都在执行这一段代码,数组大小由2扩容到4,在扩容前tab[1]=1-->5-->9。
当B线程执行到 next = e.next时让出时间片,A线程执行完整段代码可是尚未将内部的table设置为新的newTable时,线程B继续执行。
此时A线程执行完成以后,挂载在tab[1]的元素是9-->5-->1,注意这里的顺序被颠倒了。此时e = 1, next = 5;
tab[i]的按照循环次数变动顺序, 1. tab[i]=1, 2. tab[i]=5-->1, 3. tab[i]=9-->5-->1
一样B线程咱们也按照循环次数来分析
当在数组该位置get寻找对应的key的时候,就发生了死循环,引发CPU 100%问题。
而JDK8就不会出现这个问题,它在这里就有一个优化,它使用了两个指针来分别指向头节点和尾节点,并且还保证了元素本来的顺序。 固然HashMap仍然是不安全的,因此在多线程并发条件下推荐使用ConcurrentHashMap。
你的点赞是对我最大的支持,固然你关注我就更好了