HashMap源码分析

贡献博客阅读

1:modCount解释:https://www.cnblogs.com/wl889490/p/12773805.html

2:fast-fail机制:https://www.jianshu.com/p/b24f77bbfeda?ivk_sa=1024320u

3:hashmap中高位低位讲解:https://blog.csdn.net/u010425839/article/details/106620440

4:afterNodeInsertion(evict)方法作用:https://segmentfault.com/q/1010000009323139

HashMap方法初始化

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
View Code
主要作用:
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
如果有加载因子,则复制给loadFactor,如果没有加载因子,默认的加载因子是0.75
临界值大小:这里的临界值大小,是获取的初始化容量的2的最小公倍数,便于后面使用
注意:这里赋值的就是容器的大小,具体的临界值,在第一次赋值的时候,拿容器大小乘以加载因子

tableSizeFor方法如下:
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
View Code

作用:取到最小公倍数,便于后面得取模运算,因为2得最小公倍数,一定是类似:0001 0000得这种格式

2:HashMap的put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

主要看 hash(key)方法的作用:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

作用:让key的hash值得高位也能参与计算:

比如:       key.hash = 0001 0111  0101 1000

则:h >>> 16位得到:0000 0000  0001 0111

它两做异或运算得时候,原高位数不变,低位因为是异或,只有不同才会为1,所以大部分高位得也保留了,低位得基本也保留,偶尔几个相同得变为1,

从而最大可能得让高位参与到运算中来

 

再往下看putval()方法:

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         if ((tab = table) == null || (n = tab.length) == 0)
 5             n = (tab = resize()).length;
 6         if ((p = tab[i = (n - 1) & hash]) == null)
 7             tab[i] = newNode(hash, key, value, null);
 8         else {
 9             Node<K,V> e; K k;
10             if (p.hash == hash &&
11                 ((k = p.key) == key || (key != null && key.equals(k))))
12                 e = p;
13             else if (p instanceof TreeNode)
14                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15             else {
16                 for (int binCount = 0; ; ++binCount) {
17                     if ((e = p.next) == null) {
18                         p.next = newNode(hash, key, value, null);
19                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
20                             treeifyBin(tab, hash);
21                         break;
22                     }
23                     if (e.hash == hash &&
24                         ((k = e.key) == key || (key != null && key.equals(k))))
25                         break;
26                     p = e;
27                 }
28             }
29             if (e != null) { // existing mapping for key
30                 V oldValue = e.value;
31                 if (!onlyIfAbsent || oldValue == null)
32                     e.value = value;
33                 afterNodeAccess(e);
34                 return oldValue;
35             }
36         }
37         ++modCount;
38         if (++size > threshold)
39             resize();
40         afterNodeInsertion(evict);
41         return null;
42     }
View Code

第4行,判断tab是否为空,或者长度为0,对于首次进来,肯定是的,这时候就要进行tab的初始化

 

resize()方法如下:

 1 final Node<K,V>[] resize() {
 2         Node<K,V>[] oldTab = table;
 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4         int oldThr = threshold;
 5         int newCap, newThr = 0;
 6         if (oldCap > 0) {
 7             if (oldCap >= MAXIMUM_CAPACITY) {
 8                 threshold = Integer.MAX_VALUE;
 9                 return oldTab;
10             }
11             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12                      oldCap >= DEFAULT_INITIAL_CAPACITY)
13                 newThr = oldThr << 1; // double threshold
14         }
15         else if (oldThr > 0) // initial capacity was placed in threshold
16             newCap = oldThr;
17         else {               // zero initial threshold signifies using defaults
18             newCap = DEFAULT_INITIAL_CAPACITY;
19             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20         }
21         if (newThr == 0) {
22             float ft = (float)newCap * loadFactor;
23             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24                       (int)ft : Integer.MAX_VALUE);
25         }
26         threshold = newThr;
27         @SuppressWarnings({"rawtypes","unchecked"})
28             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29         table = newTab;
30         if (oldTab != null) {
31             for (int j = 0; j < oldCap; ++j) {
32                 Node<K,V> e;
33                 if ((e = oldTab[j]) != null) {
34                     oldTab[j] = null;
35                     if (e.next == null)
36                         newTab[e.hash & (newCap - 1)] = e;
37                     else if (e instanceof TreeNode)
38                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39                     else { // preserve order
40                         Node<K,V> loHead = null, loTail = null;
41                         Node<K,V> hiHead = null, hiTail = null;
42                         Node<K,V> next;
43                         do {
44                             next = e.next;
45                             if ((e.hash & oldCap) == 0) {
46                                 if (loTail == null)
47                                     loHead = e;
48                                 else
49                                     loTail.next = e;
50                                 loTail = e;
51                             }
52                             else {
53                                 if (hiTail == null)
54                                     hiHead = e;
55                                 else
56                                     hiTail.next = e;
57                                 hiTail = e;
58                             }
59                         } while ((e = next) != null);
60                         if (loTail != null) {
61                             loTail.next = null;
62                             newTab[j] = loHead;
63                         }
64                         if (hiTail != null) {
65                             hiTail.next = null;
66                             newTab[j + oldCap] = hiHead;
67                         }
68                     }
69                 }
70             }
71         }
72         return newTab;
73     }
View Code

第一次进来的话只会走到 第16行,第21行处

作用:

1:16行,将newCap赋值为threadhold,上面说过就是2的最小公倍数,也是容器的初始化大小

2:21行,容器乘以加载因子(0.75),进行容器的临界值大小的计算

3:返回给tab数组

resize()方法之后:

threadhold 已经初始化,为容器大小乘以加载因子

table已经被赋值为初始化好的tab

回到上面putVal()方法:

代码执行到第6行,判断当前的tab下标的元素是否为空,当然第一次进来肯定为空,这时候执行到第7行,将当前存放的key和value存放到

当前所属的下标下面,代码执行完毕

第6行详解:if ((p = tab[i = (n - 1) & hash]) == null)

n-1:为啥要减去1,因为n是容器的容量,而容器的容量为2的最小公倍数,减去一个可以快速取余,且不会超过容器的容量。假设容量为16举例

比如:16的二进制为:0001 0000   16-1=15的二进制为:0000 1111

hash 的值不管为多少比如:

hash:          0011 0101 0011 0001

 15二进制:0000 0000 0000 1111

进行求与运算,不管hash的值多大,高位全部为0,只有低位的四位参与计算,相同才会保留为1,也就是hash % n 求余

接着往下:

++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
这几行代码的作用:
1:modCount是记录容器改变的次数,只要是添加,就会自增1,作用:防止容器使用迭代器进行遍历的时候,对容器进行添加。
其实就是HashMap是线程不安全导致的,
使用的fail-fast机制,具体的fial-fast的机制可以看上面的博客,或者下面说明。

2:后面if代码的判断是否超过临界值了,超过了就进行扩容

3:afterNodeInsertion(evict); 这个博客解释的很清楚,不属于hashMap源码范围:https://segmentfault.com/q/1010000009323139

接着扩容来讲:
当容器的大小超过了临界值的时候,就要进行扩容了,扩容调用的方法还是resize()方法:
 1 final Node<K,V>[] resize() {
 2         Node<K,V>[] oldTab = table;
 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4         int oldThr = threshold;
 5         int newCap, newThr = 0;
 6         if (oldCap > 0) {
 7             if (oldCap >= MAXIMUM_CAPACITY) {
 8                 threshold = Integer.MAX_VALUE;
 9                 return oldTab;
10             }
11             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12                      oldCap >= DEFAULT_INITIAL_CAPACITY)
13                 newThr = oldThr << 1; // double threshold
14         }
15         else if (oldThr > 0) // initial capacity was placed in threshold
16             newCap = oldThr;
17         else {               // zero initial threshold signifies using defaults
18             newCap = DEFAULT_INITIAL_CAPACITY;
19             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20         }
21         if (newThr == 0) {
22             float ft = (float)newCap * loadFactor;
23             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24                       (int)ft : Integer.MAX_VALUE);
25         }
26         threshold = newThr;
27         @SuppressWarnings({"rawtypes","unchecked"})
28             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29         table = newTab;
30         if (oldTab != null) {
31             for (int j = 0; j < oldCap; ++j) {
32                 Node<K,V> e;
33                 if ((e = oldTab[j]) != null) {
34                     oldTab[j] = null;
35                     if (e.next == null)
36                         newTab[e.hash & (newCap - 1)] = e;
37                     else if (e instanceof TreeNode)
38                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39                     else { // preserve order
40                         Node<K,V> loHead = null, loTail = null;
41                         Node<K,V> hiHead = null, hiTail = null;
42                         Node<K,V> next;
43                         do {
44                             next = e.next;
45                             if ((e.hash & oldCap) == 0) {
46                                 if (loTail == null)
47                                     loHead = e;
48                                 else
49                                     loTail.next = e;
50                                 loTail = e;
51                             }
52                             else {
53                                 if (hiTail == null)
54                                     hiHead = e;
55                                 else
56                                     hiTail.next = e;
57                                 hiTail = e;
58                             }
59                         } while ((e = next) != null);
60                         if (loTail != null) {
61                             loTail.next = null;
62                             newTab[j] = loHead;
63                         }
64                         if (hiTail != null) {
65                             hiTail.next = null;
66                             newTab[j + oldCap] = hiHead;
67                         }
68                     }
69                 }
70             }
71         }
72         return newTab;
73     }
View Code

第:2,3,4行执行完成之后,oldTab就是当前的容器,oldCap 和oldThr就是当前容器的大小和临界值

第:11 行肯定都是满足的,这时候就需要进行扩容了,容器的大小和临界值的大小都乘以2,代码执行完成之后如下:

newCap = oldCap * 2    newThr = oldThr * 2

第:26 ,28,29行,把新的临界值赋值给临界值变量,初始化新的容器,将新的容器赋值给table变量

重点来了:

代码执行到31行,进行老容器变量的copy,开始进行遍历

1:35行,如果老容器的节点不为空,且没有链表,就一个,那就拿当前的hash 值和新容器进行求余,存放到新容器的节点上

2:39行,如果老容器的节点不为空,且是链表,这时候所有的链表上的节点都要重新求余,再存放

说明:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
这四个节点的含义:lo 表示低位 是low的缩写 而 hi是高位,high的缩写 ,可以参看上面的博客:https://blog.csdn.net/u010425839/article/details/106620440

什么是高位,什么是低位,怎么判断的?

结合代码第45行:
if ((e.hash & oldCap) == 0)
e.hash & oldCap == 0 就是低位,不等于0就是高位,为啥这样说。 当oldCap等于16举例子
比如: 16的二进制为:0001 0000
其他数的二进制,不超过16的二进制:0000 1111这是最大的,这两个数进行“与运算”还是0,其实不管怎么算,只要不超过16,低位不管是0还是1最后都是0,
所以超过16的算高位,小于16的算低位。

第二:小于16的,那么在新容器里面,还是原来的位置,很好立即,求余肯定保持不变,而大于16的求余就是 16 加上和 16求余的余数
比如:2 % 16 ==2 把容器换成32 2 % 32 == 2
比如:18 % 16 == 2 把容器换成32 18 % 32 == 18 也就是 18 % 16 + 16 就是18和老容器求余,加上老容器大小

所以代码大概意思就是:
第一:遍历某个节点上的链表

第二:判断该节点的hash值是高位,还是低位 。 低位放在低位的链表 高位放在高位的链表

第三:低位的在新容器位置不变,直接放在新容器的原位置节点,高位链表,放在老容器大小 + 原来的位置也就是上面解释的内容

什么是fast-fail机制:

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出
ConcurrentModificationException,
这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改
都将增加这个值,
那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,
如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
所以建议当大家遍历那些非线程安全的数据结构时,尽量使用迭代器
 
总结:
在集合中的任何操作都会导致 modCount变量自增,而当我们声明一个迭代器的时候,他会初始化一个值expectedModCount = modCount
当用另一个线程操作集合导致modCount自增,而expectedModCount的值不会改变,最终在查询的时候会导致 if (modCount != expectedModCount)
代码不成立从而抛出异常
 
posted @ 2022-07-06 14:31  xzlnuli  阅读(27)  评论(0编辑  收藏  举报