HashMap总结

一 Java7/8 中的 HashMap 和 ConcurrentHashMap 源码分析

http://www.importnew.com/28263.html?replytocom=643805#respond  (强烈推荐 )

Java7 HashMap

HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。

链表中存储的是Entry,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍,默认16。

loadFactor:负载因子,默认为 0.75。

threshold:扩容的阈值,等于 capacity * loadFactor,当插入的个数大于这个数时便会扩容。

put 过程分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public V put(K key, V value) {

    // 当插入第一个元素的时候,需要先初始化数组大小

    if (table == EMPTY_TABLE) {

        inflateTable(threshold);

    }

    // 如果 key 为 null,最终会将这个 entry 放到 table[0] 中

    if (key == null)

        return putForNullKey(value);

    // 1. 求 key 的 hash 值

    int hash = hash(key);

    // 2. 找到对应的数组下标

    int i = indexFor(hash, table.length);

    // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,

    //    如果有,直接覆盖,put 方法返回旧值就结束了

    for (Entry<K,V> e = table[i]; e != null; e = e.next) {

        Object k;

        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

            V oldValue = e.value;

            e.value = value;

            e.recordAccess(this);

            return oldValue;

        }

    }

 

    modCount++;

    // 4. 不存在重复的 key,将此 entry 添加到链表中

    addEntry(hash, key, value, i);

    return null;

}

计算数组下标方法:  hash & (table.length-1);

get 过程分析

  1. 根据 key 计算 hash 值。
  2. 找到相应的数组下标:hash & (length – 1)。
  3. 遍历该数组位置处的链表,直到找到相等(==或equals)的 key。

1

2

3

4

5

6

7

8

9

public V get(Object key) {

    // 之前说过,key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了

    if (key == null)

        return getForNullKey();

    //

    Entry<K,V> entry = getEntry(key);

 

    return null == entry ? null : entry.getValue();

}

getEntry(key):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

final Entry<K,V> getEntry(Object key) {

    if (size == 0) {

        return null;

    }

 

    int hash = (key == null) ? 0 : hash(key);

    // 确定数组下标,然后从头开始遍历链表,直到找到为止

    for (Entry<K,V> e = table[indexFor(hash, table.length)];

         e != null;

         e = e.next) {

        Object k;

        if (e.hash == hash &&

            ((k = e.key) == key || (key != null && key.equals(k))))

            return e;

    }

    return null;

}

Java7 ConcurrentHashMap

ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment。

concurrencyLevel:并行级别、并发数、Segment 数

initialCapacity:初始容量,整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。

loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的

put 过程分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public V put(K key, V value) {

    Segment<K,V> s;

    if (value == null)

        throw new NullPointerException();

    // 1. 计算 key 的 hash 值

    int hash = hash(key);

    // 2. 根据 hash 值找到 Segment 数组中的位置 j

    //    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,

    //    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标

    int j = (hash >>> segmentShift) & segmentMask;

    // 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,

    // ensureSegment(j) 对 segment[j] 进行初始化

    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck

         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment

        s = ensureSegment(j);

    // 3. 插入新值到 槽 s 中

    return s.put(key, hash, value, false);

}

根据 hash 值很快就能找到相应的 Segment,之后就是 Segment 内部的 put 操作了。

Segment 内部是由 数组+链表 组成的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

    // 在往该 segment 写入前,需要先获取该 segment 的独占锁

  

    HashEntry<K,V> node = tryLock() ? null :

        scanAndLockForPut(key, hash, value);

    V oldValue;

    try {

        // 这个是 segment 内部的数组

        HashEntry<K,V>[] tab = table;

        // 再利用 hash 值,求应该放置的数组下标

        int index = (tab.length - 1) & hash;

        // first 是数组该位置处的链表的表头

        HashEntry<K,V> first = entryAt(tab, index);

 

        // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况

        for (HashEntry<K,V> e = first;;) {

            if (e != null) {

                K k;

                if ((k = e.key) == key ||

                    (e.hash == hash && key.equals(k))) {

                    oldValue = e.value;

                    if (!onlyIfAbsent) {

                        // 覆盖旧值

                        e.value = value;

                        ++modCount;

                    }

                    break;

                }

                // 继续顺着链表走

                e = e.next;

            }

            else {

                // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。

                // 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。

                if (node != null)

                    node.setNext(first);

                else

                    node = new HashEntry<K,V>(hash, key, value, first);

 

                int c = count + 1;

                // 如果超过了该 segment 的阈值,这个 segment 需要扩容

                if (c > threshold && tab.length < MAXIMUM_CAPACITY)

                    rehash(node); // 扩容的是segment内部的某个数组

                else

                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,

                    // 其实就是将新的节点设置成原链表的表头

                    setEntryAt(tab, index, node);

                ++modCount;

                count = c;

                oldValue = null;

                break;

            }

        }

    } finally {

        // 解锁

        unlock();

    }

    return oldValue;

}

get 过程分析

  1. 计算 hash 值,找到 segment 数组中的具体位置。
  2. 槽中也是一个数组,根据 hash 找到数组中具体的位置
  3. 到这里是链表了,顺着链表进行查找即可

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public V get(Object key) {

    Segment<K,V> s; // manually integrate access methods to reduce overhead

    HashEntry<K,V>[] tab;

    // 1. hash 值

    int h = hash(key);

    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;

    // 2. 根据 hash 找到对应的 segment

    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&

        (tab = s.table) != null) {

        // 3. 找到segment 内部数组相应位置的链表,遍历

        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile

                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);

             e != null; e = e.next) {

            K k;

            if ((k = e.key) == key || (e.hash == h && key.equals(k)))

                return e.value;

        }

    }

    return null;

}

Java8 HashMap

由 数组+链表+红黑树 组成。当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别.

put 过程分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true);

}

 

// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作

// 第四个参数 evict 我们这里不关心

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

               boolean evict) {

    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度

    // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量

    if ((tab = table) == null || (n = tab.length) == 0)

        n = (tab = resize()).length;

    // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了

    if ((p = tab[i = (n - 1) & hash]) == null)

        tab[i] = newNode(hash, key, value, null);

 

    else {// 数组该位置有数据

        Node<K,V> e; K k;

        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点

        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 {

            // 到这里,说明数组该位置上是一个链表

            for (int binCount = 0; ; ++binCount) {

                // 插入到链表的最后面(Java7 是插入到链表的最前面)

                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);

                    // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个

                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                        treeifyBin(tab, hash);

                    break;

                }

                // 如果在该链表中找到了"相等"的 key(== 或 equals)

                if (e.hash == hash &&

                    ((k = e.key) == key || (key != null && key.equals(k))))

                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node

                    break;

                p = e;

            }

        }

        // e!=null 说明存在旧值的key与要插入的key"相等"

        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值

        if (e != null) {

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

                e.value = value;

            afterNodeAccess(e);

            return oldValue;

        }

    }

    ++modCount;

    // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容

    if (++size > threshold)

        resize();

    afterNodeInsertion(evict);

    return null;

}

和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容,不过这个不重要。

get 过程分析

  1. 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
  2. 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
  3. 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
  4. 遍历链表,直到找到相等(==或equals)的 key

1

2

3

4

public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(hash(key), key)) == null ? null : e.value;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (first = tab[(n - 1) & hash]) != null) {

        // 判断第一个节点是不是就是需要的

        if (first.hash == hash && // always check first node

            ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        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;

}

Java8 ConcurrentHashMap

结构与Java8 HashMap 基本一样。采用CAS和synchronized来保证并发安全,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发

put 过程分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

public V put(K key, V value) {

    return putVal(key, value, false);

}

final V putVal(K key, V value, boolean onlyIfAbsent) {

    if (key == null || value == null) throw new NullPointerException();

    // 得到 hash 值

    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)

            // 初始化数组,后面会详细介绍

            tab = initTable();

 

        // 找该 hash 值对应的数组下标,得到第一个节点 f

        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

            // 如果数组该位置为空,

            //    用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了

            //          如果 CAS 失败,那就是有并发操作,进到下一个循环就好了

            if (casTabAt(tab, i, null,

                         new Node<K,V>(hash, key, value, null)))

                break;                   // no lock when adding to empty bin

        }

        // hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容

        else if ((fh = f.hash) == MOVED)

            // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了

            tab = helpTransfer(tab, f);

 

        else { // 到这里就是说,f 是该位置的头结点,而且不为空

 

            V oldVal = null;

            // 获取数组该位置的头结点的监视器锁

            synchronized (f) {

                if (tabAt(tab, i) == f) {

                    if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表

                        // 用于累加,记录链表的长度

                        binCount = 1;

                        // 遍历链表

                        for (Node<K,V> e = f;; ++binCount) {

                            K ek;

                            // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了

                            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;

                        }

                    }

                }

            }

            // binCount != 0 说明上面在做链表操作

            if (binCount != 0) {

                // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8

                if (binCount >= TREEIFY_THRESHOLD)

                    // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,

                    // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树

                    //    具体源码我们就不看了,扩容部分后面说

                    treeifyBin(tab, i);

                if (oldVal != null)

                    return oldVal;

                break;

            }

        }

    }

    //

    addCount(1L, binCount);

    return null;

}

get 过程分析

  1. 计算 hash 值
  2. 根据 hash 值找到数组对应位置: (n – 1) & h
  3. 根据该位置处结点性质进行相应查找
    • 如果该位置为 null,那么直接返回 null 就可以了
    • 如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
    • 如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
    • 如果以上 3 条都不满足,那就是链表,进行遍历比对即可

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public V get(Object key) {

    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

    int h = spread(key.hashCode());

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (e = tabAt(tab, (n - 1) & h)) != null) {

        // 判断头结点是否就是我们需要的节点

        if ((eh = e.hash) == h) {

            if ((ek = e.key) == key || (ek != null && key.equals(ek)))

                return e.val;

        }

        // 如果头结点的 hash 小于 0,说明 正在扩容,或者该位置是红黑树

        else if (eh < 0)

            // 参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)

            return (p = e.find(h, key)) != null ? p.val : null;

 

        // 遍历链表

        while ((e = e.next) != null) {

            if (e.hash == h &&

                ((ek = e.key) == key || (ek != null && key.equals(ek))))

                return e.val;

        }

    }

    return null;

}

 

二  关于HashMap死循环

在扩容时发生,两线程对同一HashMap进行扩容,导致产生循环链表。

https://coolshell.cn/articles/9606.html

(看图片就能理解)

 

三   HashMap里面的数组size必须是2的次幂?

当用hash值进行数组下标匹配的时候,为了使每个链表分配均匀(防止有的链表存储数据过多,有的过少),应当使数组的size为2的次幂。

  int index(String key) {            //求数组下标的算法
          int hash = hash(key.hashCode());          
        return hash & (LOCK_NUM - 1);  

}

当 LOCK_NUM  为2的次幂时,hash值 &LOCK_NUM产生的结果分布均匀。

原文:https://nanguocoffee.iteye.com/blog/907824

 

四 HashMap为何从头插入改为尾插入

JDK1.7的头插法  JDK1.8的尾插法

HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

总结下HashMap在1.7和1.8之间的变化:

  • 1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
  • 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
  • 1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法。

作者:SevenBlue
链接:https://juejin.im/post/5ba457a25188255c7b168023
 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值