1.前言

  看完咕泡Jack前辈的有关hashMap的视频(非宣传,jack自带1.5倍嘴速,高效),收益良多,所以记录一下学习到的东西。

2.基础用法

  

  源码的注释首先就介绍了哈希表是基于Map接口,所以它的用法和其他集合的用法差不多。

  1. /**
  2. * Hash table based implementation of the <tt>Map</tt> interface. This * 哈希表的实现基于<tt>Map</ tt>接口。
  3. * implementation provides all of the optional map operations, and permits * 此实现提供所有可选的映射操作,
  4. * <tt>null</tt> values and the <tt>null</tt> key. (The <tt>HashMap</tt> * 并允许<tt> null </ tt>值和<tt> null </ tt>键。
  5. * class is roughly equivalent to <tt>Hashtable</tt>, except that it is * (<tt> HashMap </ tt>类与<tt> Hashtable </ tt>大致等效,
  6. * unsynchronized and permits nulls.) This class makes no guarantees as to * 除了它是不同步的,并且允许为null。) , 此类不保证映射的顺序。
  7. * the order of the map; in particular, it does not guarantee that the order * 特别是不能保证订单将随着时间的推移保持不变。
  8. * will remain constant over time.

  对应的源码,如图所示,它继承了抽象Map类,实现了Map接口:

       public class HashMap<K,V> extends AbstractMap<K,V>  implements Map<K,V>, Cloneable, Serializable { ... }

     至于具体咋用,不多介绍,推一个链接,HashMap的基础用法:https://blog.csdn.net/lzx_cherry/article/details/98947819

3.存储方式

下面就是介绍一个HashMap完成put(key, value)操作之后的存储流程。

  (1)HashMap key、value被put后的存储方式:

    在JDK1.7及其之前都是用的 数组+链表 的方式,JDK1.8之后存储方式优化成了 数组+链表+红黑树 的方式。

  (JDK1.8后,如果单链表存储的长度大于8则转换为红黑树存储,采用这样的改善有利于解决hash冲突中链表过长引发的性能下降问题)

  (2)图解HashMap的主要数据结构:

     

  <1>存储单元 Node

  图中的每一个格子代表每一个Node对象。Node的信息主要包含它的存储位置,key,value,如果在链表中则会有下一个Node的信息,如果存储在红黑树中则包含红黑树的相关信息。

  由上面我们可以写出Node数据结构的伪代码:
      Node[] table;   数组
      class Node{ Node next; }  链表
      TreeNode(left, right, parent, boolean flag = red| black)  红黑树

  而HashMap源码中Node的代码和上面伪代码的一致:

  1. /**
  2. * Basic hash bin node, used for most entries. (See below for
  3. * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
  4. */
  5. static class Node<K,V> implements Map.Entry<K,V> {
  6. //通过hash算法得出的存储位置
  7. final int hash;
  8. //key
  9. final K key;
  10. //value
  11. V value;
  12. //链表的下个Node
  13. Node<K,V> next;
         ...
      }

  HashMap源码中TreeMap的代码(建议之前先了解红黑树的原理):

  1. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  2. TreeNode<K,V> parent; // red-black tree links
  3. TreeNode<K,V> left;
  4. TreeNode<K,V> right;
  5. TreeNode<K,V> prev; // needed to unlink next upon deletion
  6. boolean red;
        ... }

  <2>存储过程

  根据HashMap的数据结构,可以大致推断出它的存储过程。

    a.先创建一个数组

    b.计算出存储key value的Node的位置

    c.如果hash冲突了,判断冲突数目的长度决定使用链表还是红黑树结构

    d.数组不够需要进行扩容

  下面对存储过程进行细致的分析。

  1.计算出存储key value的Node的位置

  先分析这个有助于其他的理解,这也是理解存储过程一个比较基础重要的内容。

  计算出Node的位置,就是需要得到Node在数组中的整型数下标,但是前提是不超出数组的大小。HashMap数组的大小可以采用默认值,也可以自行规定,这边我们采用默认的值16进行分析。

  首先,要保证是个整型数,最好还是和key value有关联的,所以最好的方式就是通过key.hashCode()。其次,我们需要保证Node的index大小在0~15之间。所以我们可以先进行一个取余判断,判断: 整型数%16 = [ 0 , 15 ]。

   分析取余:

    例如一个数字1,hashCode的值为49,那么取余的操作就为 49 % 16 = 1。但是这样的取模方式还可以进行优化,49的10进制整型数转化为32位二进制:
                    0000 0000 0000 0000 0000 0000 0011 0001 % 16 = 1
      对16进行取余,其实效果就相当于对(16-1)进行与操作:0000 0000 0000 0000 0000 0000 0011 0001 & 0111,因为与操作时候与的时候,最后四位的范围是[0,15],如果大于15的话,就进位了,这样可以更有效控制整形数的范围。     

         0000 0000 0000 0000 0000 0000 0011 0001 
                                                        0 1111  &操作   (数组大小 - 1)
           ————————————————————————————————————————————————
                                                         0001(结果)

  最终返回的结果就是Node在数组中的位置index了。index如果相同的话就会产生位置冲突,这时候就需要链表和红黑树数据结构,但这样会使得我们去获取key value变得更加耗时。所以我们需要尽量保证index就是Node的位置不要太容易就出现重复的情况。

   从上的与过程中我们可以看出,能决定Node位置取决在两个相与的数(暂称为key1和key2),这两个数的后4位决定了Node的位置,如果要保证hash不冲突的话,就要先分析他们。与操作,一方为0就结果为0,key2的最后四位值如果一个为0的话,无论key1对应的是什么,结果都是0,这样极其容易导致冲突,所以我们要尽量保证key2除了最高为0外。其他位置都1。例如:01111(15)、011111(31)、0111111(63),不难看出key2的值,其实就是2的n次幂-1。所以我们需要尽量保证数组的大小为2的n次幂。但是即便保证了后四位都为1的话,毕竟只有4位,4位进行与操作,还是很容易出现一个重复情况,对于这种情况,HashMap采用了异或(xor)操作( a⊕b = (¬a ∧ b) ∨ (a ∧¬b) )。

  具体操作,将32位的二进制数字一分为二,左边16位的高16位为一份,右边的低16位为另一份。两者进行异或运算,降低重复的可能性。

        如:

            高16位                               低16位

        0101 0101 0010 1001   |   0001 0001 0001 0110

  其实这就是HashMap中的hash算法,源码:

  1. static final int hash(Object key) {
  2. int h;
    //如果key为null则返回0,如果不为null则返回key的hashCode高低16位异或运算的结果
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }

  如果再重复就只能形成链表和红黑树了。

  2.创建数组以及数组的扩容

  如果采用默认的大小的话,默认的数组大小为16。源码:

  1. /**
    * The default initial capacity - MUST be a power of two.
       * 默认初始容量为16,必须为2的幂
       * 表示1,左移4位,变成10000,也就是16
  2. */
  3. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

至于为什么不采用int DEFAULT_INITIAL_CAPACITY = 16;,一方面是省略了中间一些复杂的转换过程,直接以二进制形式去运行运算,另一方面也是配合2次幂的约束条件。

  当然我们知道,HashMap数组的大小也是可以自己定义的。自己定义和默认有啥区别,如果你使用了阿里的checkstyle,初始化HashMap使用了默认的大小,这时候规约就会提示你需要自己定义

HashMap的大小。我们可以看一下阿里巴巴的规约:

  上面说的很清楚了,如果不指定初始化的大小,容易引起多次的扩容操作,影响性能。并给出了推荐的初始化值 = (需要存储的元素个数 / 负载因子) + 1;

  负载因子决定了数组扩容的阔值,如果一个数组大小为20,负载因子为0.75,那么数组长度到达15的时候,数组就会进行扩容操作。15也就是数组扩容的阔值,0.75就称为负载因子,附上源码:

  源码中的load factor也就是负载因子,规定的大小为0.75,也就是3/4。

  1. /**
  2. * The load factor used when none specified in constructor.
  3. */
  4. static final float DEFAULT_LOAD_FACTOR = 0.75f;

  如果自己进行初始化数组的值,那么是不是就可以随意设置值了呢?看一下源码就知道了:

  初始化大小必须大于等于0,且是有最大值的。

  1. /**
  2. * Constructs an empty <tt>HashMap</tt> with the specified initial
  3. * capacity and load factor.
  4. *
  5. * @param initialCapacity the initial capacity 初始化大小
  6. * @param loadFactor the load factor 负载因子
  7. * @throws IllegalArgumentException if the initial capacity is negative
  8. * or the load factor is nonpositive
  9. */
  10. public HashMap(int initialCapacity, float loadFactor) {
  11. //如果初始化大小小于0,抛出异常
  12. if (initialCapacity < 0)
  13. throw new IllegalArgumentException("Illegal initial capacity: " +
  14. initialCapacity);
  15. //如果初始化大小大于最大值,则将初始化值设为最大值
  16. if (initialCapacity > MAXIMUM_CAPACITY)
  17. initialCapacity = MAXIMUM_CAPACITY;
  18. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  19. throw new IllegalArgumentException("Illegal load factor: " +
  20. loadFactor);
  21. this.loadFactor = loadFactor;
  22. //阔值,该方法保证了数组初始化大小为2的次幂
  23. this.threshold = tableSizeFor(initialCapacity);
  24. }

  最大值:(1073741824)

  1. static final int MAXIMUM_CAPACITY = 1 << 30;

  如果你初始化数组大小时候,没有按照前面的要求将数组的大小设为n的二次幂,也就是key2不是0111111这样子的形式,是不是会增加到hash冲突的概率呢。其实,HashMap源码里面针对这种情况进行了

调整,保证了每一个数组大小为2的次幂,具体源码看下面“

  1. /**
  2. * Returns a power of two size for the given target capacity.
  3. */
  4. static final int tableSizeFor(int cap) {
  5. int n = cap - 1;
  6. //位或操作,一步一步保证最后几位都1
  7. n |= n >>> 1;
  8. n |= n >>> 2;
  9. n |= n >>> 4;
  10. n |= n >>> 8;
  11. n |= n >>> 16;
  12. //如果n小于0返回1,不然返回小于最大值的n+1值
  13. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  14. }

  上面就是哈希中数组初始化的内容,接下来说一下数组扩容的方式。

  1.需要先知道的是,扩容是把原始的数组扩成多大的容量。

  从源码中的must be power of two,就是必须是2的n次幂就可以推断出扩容的方式是double,也就是将数组的大小翻倍。

  2.新数组如何创建,以及如何重新散列。(重新散列:把老数组中的Node移到到新的数组。)

  新数组的大小我们已经可以确定为旧数组大小的2倍,现在主要的问题就是重新散列,也就是把旧的数组中Node转移到新的数组中去。普遍的做法就是遍历旧的数组,将非空的Node依次赋值给新的数组。

如果Node节点下面是链表就遍历链表,再赋值给新数组的Node节点。如果是红黑树,也是一样先打散再重排。这样的做法理解起来很简单,让我们一起看一下源码(较长):

  1. /**
  2. *
  3.      *初始化或增加表大小。 如果为null,则分配
  4.      *符合在现场阈值中保持的初始容量目标。
  5.      *否则,因为我们使用的是二次幂扩展,所以
  6.      *每个bin中的元素必须保持相同的索引或移动
  7.      *在新表中具有两个偏移量的幂。
  8.      *
  9. * Initializes or doubles table size. If null, allocates in
  10. * accord with initial capacity target held in field threshold.
  11. * Otherwise, because we are using power-of-two expansion, the
  12. * elements from each bin must either stay at same index, or move
  13. * with a power of two offset in the new table.
  14. *
  15. * @return the table
  16. */
  17. final Node<K,V>[] resize() {
  18. //旧的Node数组
  19. Node<K,V>[] oldTab = table;
  20. //获取旧的数组长度,如果为null则返回0
  21. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  22. //旧数组的阔值
  23. //threshold :The next size value at which to resize (capacity * load factor).
  24. //下一个要调整大小的大小值(容量*负载系数)。
  25. int oldThr = threshold;
  26. //新的数组和新的阔值
  27. int newCap, newThr = 0;
  28. //如果旧数组长度大于0
  29. if (oldCap > 0) {
  30. //限制最大值
  31. if (oldCap >= MAXIMUM_CAPACITY) {
  32. threshold = Integer.MAX_VALUE;
  33. return oldTab;
  34. }
  35. //限制新的阔值大小
  36. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  37. oldCap >= DEFAULT_INITIAL_CAPACITY)
  38. //这边就是对新的数组阔值进行翻倍
  39. newThr = oldThr << 1; // double threshold
  40. }
  41. //初始化新数组的值
  42. else if (oldThr > 0) // initial capacity was placed in threshold
  43. newCap = oldThr;
  44. else { // zero initial threshold signifies using defaults
  45. //如果之前的阔值小于0,新的数组大小设置为16,阔值设置为 16 * 0.75f
  46. newCap = DEFAULT_INITIAL_CAPACITY;
  47. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  48. }
  49. if (newThr == 0) {
  50. //如果之前的阔值=0,赋值给新的阔值
  51. float ft = (float)newCap * loadFactor;
  52. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  53. (int)ft : Integer.MAX_VALUE);
  54. }
  55. //全局变量的阔值变化
  56. threshold = newThr;
  57. @SuppressWarnings({"rawtypes","unchecked"})
  58. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  59. //新的哈希表
  60. table = newTab;
  61. if (oldTab != null) {
  62. //不为空的情况下遍历
  63. for (int j = 0; j < oldCap; ++j) {
  64. Node<K,V> e;
  65. //如果Node节点不为null
  66. if ((e = oldTab[j]) != null) {
  67. //之前的值删除(就是设置为null)
  68. oldTab[j] = null;
  69. //如果不为链表和红黑树
  70. if (e.next == null)
  71. //直接赋值给新的哈希表
  72. newTab[e.hash & (newCap - 1)] = e;
  73. //如果Node是红黑树数据结构,打散重排
  74. else if (e instanceof TreeNode)
  75. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  76. else { // preserve order
  77. //不然就是链表,对链表进行遍历 赋值到新的的哈希表
  78. Node<K,V> loHead = null, loTail = null;
  79. Node<K,V> hiHead = null, hiTail = null;
  80. Node<K,V> next;
  81. do {
  82. next = e.next;
  83. if ((e.hash & oldCap) == 0) {
  84. if (loTail == null)
  85. loHead = e;
  86. else
  87. loTail.next = e;
  88. loTail = e;
  89. }
  90. else {
  91. if (hiTail == null)
  92. hiHead = e;
  93. else
  94. hiTail.next = e;
  95. hiTail = e;
  96. }
  97. } while ((e = next) != null);
  98. if (loTail != null) {
  99. loTail.next = null;
  100. newTab[j] = loHead;
  101. }
  102. if (hiTail != null) {
  103. hiTail.next = null;
  104. newTab[j + oldCap] = hiHead;
  105. }
  106. }
  107. }
  108. }
  109. }
  110. return newTab;
  111. }

  但是在赋值的过程中,需要注意所有的位置都进行了新的一轮hash运算,在 【1.计算出存储key value的Node的位置 】中可以知道key2的值形式要保持是01111……11的形式。

  之前的操作是这样的:

       0000 0000 0000 0000 0000 0000 0011 0001 
                                                           0 1111  &操作   (数组大小 - 1)
           ————————————————————————————————————————————————
                                                              0001(结果)  --- 1

   但是我们现在对key2的值进行了翻倍,那么随之与操作的结果也会变化,也就是在新的数组中Node的位置以及发生了变化,具体看下面:

     0000 0000 0000 0000 0000 0000 0010 0001      key1的值 第一种情况

           0000 0000 0000 0000 0000 0000 001 0001      key1的值 第二种情况
                                                           01 1111  &操作   (新数组大小 = 旧数组大小 * 2,比之前左边多了一位1)
           ————————————————————————————————————————————————

                                   00  0001(第一种结果) ---1

第一种情况,key1的值和之前的值一样,也就是重新散列的位置不变
                                                          01  0001(第二种结果) ---17     

第二种情况,key1的值比之前的值大16(数组的长度),也就是重新散列的位置发生了变化

  所以,哈希resize后,之前旧数组的Node在新数组中的位置有两种情况:1.保持和旧数组一样  2.旧数组的位置+旧数组的大小

  在注释部分,就交代了“刷新”操作是会重建内部数据结构的。

  1. * <p>An instance of <tt>HashMap</tt> has two parameters that affect its * <p> <tt> HashMap </ tt>的实例有两个参数会影响其性能:<i>初始容量</ i>和<i>负载系数</ i>。
  2. * performance: <i>initial capacity</i> and <i>load factor</i>. The * 容量只是创建哈希表时的容量。
  3. * <i>capacity</i> is the number of buckets in the hash table, and the initial * <i>负载因子</ i>是衡量哈希表允许填充的程度的度量在容量自动增加之前获取
  4. * capacity is simply the capacity at the time the hash table is created. The * 当哈希表中的条目超过了负载系数和当前容量,
  5. * <i>load factor</i> is a measure of how full the hash table is allowed to * 哈希表被<i>刷新</ i>(即内部数据结构已重建),
  6. * get before its capacity is automatically increased. When the number of * 因此哈希表的大小大约是原来容量的2倍。
  7. * entries in the hash table exceeds the product of the load factor and the
  8. * current capacity, the hash table is <i>rehashed</i> (that is, internal data
  9. * structures are rebuilt) so that the hash table has approximately twice the
  10. * number of buckets.
  11. *

  3.key和value的put经历

  上面说的都是关于Node的位置问题,如果Node位置确定了,那么剩下的就只剩putNode里面的key和value了。首先,一个数组里面put一个Node,我们需要思考这个位置是否是NULL,如果为NULL的话,就在该位置new一个新的Node ;如果不为NULL,那么就需要判断put的内容是覆盖原来的value还是新增一个Node,新增又分为链表新增和红黑树的新增。具体的源码如下:

  1. /**
  2. * Implements Map.put and related methods 实现Map.put及相关方法
  3. *
  4. * @param hash hash for key hash算法算出的Node位置
  5. * @param key the key 键
  6. * @param value the value to put 放置的值
  7. * @param onlyIfAbsent if true, don't change existing value onlyIfAbsent如果为true,请不要更改现有值
  8. * @param evict if false, the table is in creation mode. 退出,如果为false,则表处于创建模式。
  9. * @return previous value, or null if none 上一个值,如果没有则返回null
  10. */
  11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  12. boolean evict) {
  13. Node<K,V>[] tab; Node<K,V> p; int n, i;
  14. //初始化哈希表
  15. if ((tab = table) == null || (n = tab.length) == 0)
  16. n = (tab = resize()).length;
  17. //获取在哈希表中的位置,并且判断该位置是否为null,如果是null 直接就创建新的Node
  18. if ((p = tab[i = (n - 1) & hash]) == null)
  19. tab[i] = newNode(hash, key, value, null);
  20. else {
  21. //如果该位置不为null,则可能为链表或者红黑树
  22. Node<K,V> e; K k;
  23. //如果key值相同,hash也相同,则替换value的值
  24. if (p.hash == hash &&
  25. ((k = p.key) == key || (key != null && key.equals(k))))
  26. e = p;
  27. //红黑树存储
  28. else if (p instanceof TreeNode)
  29. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  30. //链表存储
  31. else {
  32. //遍历链表
  33. for (int binCount = 0; ; ++binCount) {
  34. if ((e = p.next) == null) {
  35. //尾部插入
  36. p.next = newNode(hash, key, value, null);
  37. //如果长度大于8 (TREEIFY_THRESHOLD = 8)
  38. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  39. //链表转换为红黑树
  40. treeifyBin(tab, hash);
  41. break;
  42. }
  43. if (e.hash == hash &&
  44. ((k = e.key) == key || (key != null && key.equals(k))))
  45. break;
  46. p = e;
  47. }
  48. }
  49. if (e != null) { // existing mapping for key
  50. V oldValue = e.value;
  51. if (!onlyIfAbsent || oldValue == null)
  52. e.value = value;
  53. afterNodeAccess(e);
  54. return oldValue;
  55. }
  56. }
  57. ++modCount;
  58. //数组长度大于阔值
  59. if (++size > threshold)
           //扩容
  60. resize();
  61. afterNodeInsertion(evict);
  62. return null;
  63. }

  getNode的源码和上面的如出一辙,也很好理解,根据hash先找到Node的头节点,如果头节点的hash和key都相同,就直接返回第一个数组的值,否在判断该Node是否是链表或者红黑树结构,再根据key获取值,贴一下:

  1. /**
  2. * Implements Map.get and related methods
  3. *
  4. * @param hash hash for key
  5. * @param key the key
  6. * @return the node, or null if none
  7. */
  8. final Node<K,V> getNode(int hash, Object key) {
  9. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  10. if ((tab = table) != null && (n = tab.length) > 0 &&
  11. (first = tab[(n - 1) & hash]) != null) {
  12. if (first.hash == hash && // always check first node
  13. ((k = first.key) == key || (key != null && key.equals(k))))
  14. return first;
  15. if ((e = first.next) != null) {
  16. if (first instanceof TreeNode)
  17. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  18. do {
  19. if (e.hash == hash &&
  20. ((k = e.key) == key || (key != null && key.equals(k))))
  21. return e;
  22. } while ((e = e.next) != null);
  23. }
  24. }
  25. return null;
  26. }

4.ConcurrentHashMap以及线程安全

  先看一下同样是线程安全的HashTable是如何保证线程安全的:

  1. public synchronized V put(K key, V value) {
  2. // Make sure the value is not null
  3. if (value == null) {
  4. throw new NullPointerException();
  5. }

  由上面的源码可以看出synchronized关键字直接约束了整个put方法,这样线程虽然是安全的,但是效率过于低下。对比之下ConcurrentHashMap的锁设计就更为精确化,因为对于一个put方法,后者把它大致分为几个步骤,通过对每个步骤进行线程安全约束来提升效率。(index:这边当作数组下标)

  大致的put步骤:map.put(K,V)—>  new Node[]创建数组 —> index == null(数组位置值为null,直接创建) —> index!=null(加入链表,红黑树) —> resize()扩容

  1.保证初始化哈希表线程安全

  在创建数组的时候,通过乐观锁机制(CAS)保证只有一个线程去初始化数组;

  初始化的源码:

  1. //putVal 方法中
    if (tab == null || (n = tab.length) == 0)
              //初始化
  2. tab = initTable();
  1. /**
  2. * Initializes table, using the size recorded in sizeCtl.
  3. */
  4. private final Node<K,V>[] initTable() {
  5. Node<K,V>[] tab; int sc;
  6. while ((tab = table) == null || tab.length == 0) {
  7. //如果SIZECTL<0,就代表已经有一个线程在执行初始化了,进行线程让步
  8. if ((sc = sizeCtl) < 0)
  9. Thread.yield(); // lost initialization race; just spin
  10. //CAS 乐观锁机制保证数组初始化线程安全,如果当前对象的值==SIZECTL,则认为线程安全,返回-1
  11. else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
  12. try {
  13. if ((tab = table) == null || tab.length == 0) {
  14. int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
  15. @SuppressWarnings("unchecked")
  16. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
  17. table = tab = nt;
  18. sc = n - (n >>> 2);
  19. }
  20. } finally {
  21. sizeCtl = sc;
  22. }
  23. break;
  24. }
  25. }
  26. return tab;
  27. }

  2.数组下标index为null时候

  如果数组下标的值为null,也是通过乐观锁机制保证线程安全,源码:

  1. /** Implementation for put and putIfAbsent */
  2. final V putVal(K key, V value, boolean onlyIfAbsent) {
  3. if (key == null || value == null) throw new NullPointerException();
  4. int hash = spread(key.hashCode());
  5. int binCount = 0;
  6. for (Node<K,V>[] tab = table;;) {
  7. Node<K,V> f; int n, i, fh;
  8. if (tab == null || (n = tab.length) == 0)
  9. tab = initTable();
  10. //如果数组下标的值为null,也是通过乐观锁机制保证线程安全
  11. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  12. if (casTabAt(tab, i, null,
  13. new Node<K,V>(hash, key, value, null)))
  14. break; // no lock when adding to empty bin
  15. }

  3.数组下标不为null

  数组下标不为null的话,那么就是链表和红黑树结构,如果再用CAS去保证线程安全就需要对链表和红黑树中的元素依次去进行compareAndSwapInt,很麻烦。所以在这边,我们可以将链表或者红黑树的头节点锁住,就可以保证一整个链表红黑树的线程安全,这样影响的范围就会缩小。

  源码:

  通过对头节点(数组下标)的锁,保证一整个链表和红黑树的线程安全。

  1. //如果数组下标的值为null,也是通过乐观锁机制保证线程安全
  2. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  3. if (casTabAt(tab, i, null,
  4. new Node<K,V>(hash, key, value, null)))
  5. break; // no lock when adding to empty bin
  6. }
  7. else if ((fh = f.hash) == MOVED)
  8. tab = helpTransfer(tab, f);
  9. else {
  10. V oldVal = null;
  11. //如果下标不为null,那么就是链表和红黑树结构,如果再用CAS去保证线程安全就需要对链表和红黑树中的元素依次去进行compareAndSwapInt,
  12. //所以在这边,我们可以将链表或者红黑树的头节点锁住,就可以保证一整个链表红黑树的线程安全
  13. synchronized (f) {
  14. if (tabAt(tab, i) == f) {
  15. if (fh >= 0) {
  16. binCount = 1;
  17. for (Node<K,V> e = f;; ++binCount) {
  18. K ek;
  19. if (e.hash == hash &&
  20. ((ek = e.key) == key ||
  21. (ek != null && key.equals(ek)))) {
  22. oldVal = e.val;
  23. if (!onlyIfAbsent)
  24. e.val = value;
  25. break;
  26. }

  完整的putVal源码:

  1. /** Implementation for put and putIfAbsent */
  2. final V putVal(K key, V value, boolean onlyIfAbsent) {
  3. if (key == null || value == null) throw new NullPointerException();
  4. int hash = spread(key.hashCode());
  5. int binCount = 0;
  6. for (Node<K,V>[] tab = table;;) {
  7. Node<K,V> f; int n, i, fh;
  8. if (tab == null || (n = tab.length) == 0)
  9. tab = initTable();
  10. //如果数组下标的值为null,也是通过乐观锁机制保证线程安全
  11. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  12. if (casTabAt(tab, i, null,
  13. new Node<K,V>(hash, key, value, null)))
  14. break; // no lock when adding to empty bin
  15. }
  16. //如果不是初始化数组的线程的话,就去帮忙重新散列
  17.        //MOVED 的值为-1
  18. else if ((fh = f.hash) == MOVED)
  19. tab = helpTransfer(tab, f);
  20. else {
  21. V oldVal = null;
  22. //如果下标不为null,那么就是链表和红黑树结构,如果再用CAS去保证线程安全就需要对链表和红黑树中的元素依次去进行compareAndSwapInt,
  23. //所以在这边,我们可以将链表或者红黑树的头节点锁住,就可以保证一整个链表红黑树的线程安全
  24. synchronized (f) {
  25. if (tabAt(tab, i) == f) {
  26. if (fh >= 0) {
  27. binCount = 1;
  28. for (Node<K,V> e = f;; ++binCount) {
  29. K ek;
  30. if (e.hash == hash &&
  31. ((ek = e.key) == key ||
  32. (ek != null && key.equals(ek)))) {
  33. oldVal = e.val;
  34. if (!onlyIfAbsent)
  35. e.val = value;
  36. break;
  37. }
  38. Node<K,V> pred = e;
  39. if ((e = e.next) == null) {
  40. pred.next = new Node<K,V>(hash, key,
  41. value, null);
  42. break;
  43. }
  44. }
  45. }
  46. else if (f instanceof TreeBin) {
  47. Node<K,V> p;
  48. binCount = 2;
  49. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  50. value)) != null) {
  51. oldVal = p.val;
  52. if (!onlyIfAbsent)
  53. p.val = value;
  54. }
  55. }
  56. }
  57. }
  58. if (binCount != 0) {
  59. if (binCount >= TREEIFY_THRESHOLD)
  60. treeifyBin(tab, i);
  61. if (oldVal != null)
  62. return oldVal;
  63. break;
  64. }
  65. }
  66. }
  67.     //每次put都会统计数组的大小,以确定是否扩容
  68. addCount(1L, binCount);
  69. return null;
  70. }

  4.扩容的线程安全

  一个线程去扩容的时候,其他的线程进行扩容或进行put操作都会引起线程安全问题,所以在一个线程在扩容的时候,其他的线程需要进入等待状态。这样等待状态的线程占据着CPU但是却不做事情,所以源码对此进行了优化。首先,保证只有一个线程能去初始化。其次,剩下等待的线程共同帮助完成重新散列。

  比如一个数组tab[]大小16,一个线程去负责扩容成大小32的新数组。剩下的等待线程就会去帮着重新散列,如:等待线程a就会去从数组末尾开始向前领取一个区间的Node进行重新散列,例如区间(tab[13]~tab[15] ),a线程去负责对区间的Node进行重新散列。如果在a完成了,还没有其他的扩容(或者put)线程进入变成等待线程的话,a就会继续领取一个区间的任务继续进行重新散列,如果有一个线程b要进行扩容,因为扩容操作已经有线程在做了,b随之进入等待状态,这时候b线程就会去帮着a线程去完成剩下区间的散列任务。以此反复,其中的每一个线程帮着完成重新散列任务都是会提交自己的进度的,所以不要担心会重复或少工作这么一个情况。

  上面的过程,侧重点就2个,第一个保证一个线程初始化数组,第二保证剩下的线程去帮助扩容。

  源码实现:

  统计源码,决定什么时候能扩容:

  1. /**
  2. * Adds to count, and if table is too small and not already
  3. * resizing, initiates transfer. If already resizing, helps
  4. * perform transfer if work is available. Rechecks occupancy
  5. * after a transfer to see if another resize is already needed
  6. * because resizings are lagging additions.
  7. *
  8. * @param x the count to add
  9. * @param check if <0, don't check resize, if <= 1 only check if uncontended
  10. */
  11. private final void addCount(long x, int check) {
  12. CounterCell[] as; long b, s;
  13. if ((as = counterCells) != null ||
  14. !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
  15. CounterCell a; long v; int m;
  16. boolean uncontended = true;
  17. if (as == null || (m = as.length - 1) < 0 ||
  18. (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
  19. !(uncontended =
  20. U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
  21. fullAddCount(x, uncontended);
  22. return;
  23. }
  24. if (check <= 1)
  25. return;
  26. //统计的结果s
  27. s = sumCount();
  28. }
  29. if (check >= 0) {
  30. Node<K,V>[] tab, nt; int n, sc;
  31. //如果s大于阔值,则需要进行扩容
  32. while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
  33. (n = tab.length) < MAXIMUM_CAPACITY) {
  34. int rs = resizeStamp(n);
  35. //sc = 阔值
  36. if (sc < 0) {
  37. if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
  38. sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
  39. transferIndex <= 0)
  40. break;
  41. if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
  42. transfer(tab, nt);
  43. }
  44. //阔值大于0 进行初始化 乐观锁保证线程安全
  45. else if (U.compareAndSwapInt(this, SIZECTL, sc,
  46. (rs << RESIZE_STAMP_SHIFT) + 2))
  47. transfer(tab, null);
  48. s = sumCount();
  49. }
  50. }
  51. }

  任务代码:  

  1. /**
  2. * Moves and/or copies the nodes in each bin to new table. See
  3. * above for explanation.
  4. */
  5. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  6. int n = tab.length, stride;
  7.      //确定任务的大小=16
  8. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  9. stride = MIN_TRANSFER_STRIDE; // subdivide range
  10.      //初始化数组线程,如果入参的nextTab为null的话
  11.  
  12. if (nextTab == null) { // initiating
  13. try {
  14. @SuppressWarnings("unchecked")
  15. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
  16. nextTab = nt;
  17. } catch (Throwable ex) { // try to cope with OOME
  18. sizeCtl = Integer.MAX_VALUE;
  19. return;
  20. }
  21. nextTable = nextTab;
  22. transferIndex = n;
  23. }
  24.      //非初始化线程,nextTab不为null
  25. int nextn = nextTab.length;
  26. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  27.      //保证true,使其不断领取任务
  28. boolean advance = true;
  29.      //标记重新散列任务是否完成
  30. boolean finishing = false; // to ensure sweep before committing nextTab
  31. for (int i = 0, bound = 0;;) {
  32. Node<K,V> f; int fh;
  33.        //领取散列任务
  34. while (advance) {
  35. int nextIndex, nextBound;
  36. if (--i >= bound || finishing)
  37. advance = false;
  38. else if ((nextIndex = transferIndex) <= 0) {
  39. i = -1;
  40. advance = false;
  41. }
  42. else if (U.compareAndSwapInt
  43. (this, TRANSFERINDEX, nextIndex,
  44. nextBound = (nextIndex > stride ?
  45. nextIndex - stride : 0))) {
  46. bound = nextBound;
  47. i = nextIndex - 1;
  48. advance = false;
  49. }
  50. }
  51.        //执行散列
  52. if (i < 0 || i >= n || i + n >= nextn) {
  53. int sc;
  54.           //完成扩容
  55. if (finishing) {
  56. nextTable = null;
  57. table = nextTab;
  58.             //扩展改变
  59. sizeCtl = (n << 1) - (n >>> 1);
  60. return;
  61. }
  62.           //没有完成扩容,汇报自己的完成任务
  63. if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
  64. if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
  65. return;
  66. finishing = advance = true;
  67. i = n; // recheck before commit
  68. }
  69. }
  70. else if ((f = tabAt(tab, i)) == null)
  71. advance = casTabAt(tab, i, null, fwd);
  72. else if ((fh = f.hash) == MOVED)
  73. advance = true; // already processed
  74. else {
  75.           //迁移数据操作,和HashMap一致
  76. synchronized (f) {
  77. if (tabAt(tab, i) == f) {
  78. Node<K,V> ln, hn;
  79. if (fh >= 0) {
  80. int runBit = fh & n;
  81. Node<K,V> lastRun = f;
  82. for (Node<K,V> p = f.next; p != null; p = p.next) {
  83. int b = p.hash & n;
  84. if (b != runBit) {
  85. runBit = b;
  86. lastRun = p;
  87. }
  88. }
  89. if (runBit == 0) {
  90. ln = lastRun;
  91. hn = null;
  92. }
  93. else {
  94. hn = lastRun;
  95. ln = null;
  96. }
  97. for (Node<K,V> p = f; p != lastRun; p = p.next) {
  98. int ph = p.hash; K pk = p.key; V pv = p.val;
  99. if ((ph & n) == 0)
  100. ln = new Node<K,V>(ph, pk, pv, ln);
  101. else
  102. hn = new Node<K,V>(ph, pk, pv, hn);
  103. }
  104. setTabAt(nextTab, i, ln);
  105. setTabAt(nextTab, i + n, hn);
  106. setTabAt(tab, i, fwd);
  107. advance = true;
  108. }
  109. else if (f instanceof TreeBin) {
  110. TreeBin<K,V> t = (TreeBin<K,V>)f;
  111. TreeNode<K,V> lo = null, loTail = null;
  112. TreeNode<K,V> hi = null, hiTail = null;
  113. int lc = 0, hc = 0;
  114. for (Node<K,V> e = t.first; e != null; e = e.next) {
  115. int h = e.hash;
  116. TreeNode<K,V> p = new TreeNode<K,V>
  117. (h, e.key, e.val, null, null);
  118. if ((h & n) == 0) {
  119. if ((p.prev = loTail) == null)
  120. lo = p;
  121. else
  122. loTail.next = p;
  123. loTail = p;
  124. ++lc;
  125. }
  126. else {
  127. if ((p.prev = hiTail) == null)
  128. hi = p;
  129. else
  130. hiTail.next = p;
  131. hiTail = p;
  132. ++hc;
  133. }
  134. }
  135. ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
  136. (hc != 0) ? new TreeBin<K,V>(lo) : t;
  137. hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
  138. (lc != 0) ? new TreeBin<K,V>(hi) : t;
  139. setTabAt(nextTab, i, ln);
  140. setTabAt(nextTab, i + n, hn);
  141. setTabAt(tab, i, fwd);
  142. advance = true;
  143. }
  144. }
  145. }
  146. }
  147. }
  148. }

  再精细的就不会了。

(Concurrent)HashMap的存储过程及原理。的更多相关文章

  1. 详解HashMap的内部工作原理

    本文将用一个简单的例子来解释下HashMap内部的工作原理.首先我们从一个例子开始,而不仅仅是从理论上,这样,有助于更好地理解,然后,我们来看下get和put到底是怎样工作的. 我们来看个非常简单的例 ...

  2. 走向DBA[MSSQL篇] 针对大表 设计高效的存储过程【原理篇】 附最差性能sql语句进化过程客串

    原文:走向DBA[MSSQL篇] 针对大表 设计高效的存储过程[原理篇] 附最差性能sql语句进化过程客串 测试的结果在此处 本篇详解一下原理 设计背景 由于历史原因,线上库环境数据量及其庞大,很多千 ...

  3. 关于HashMap put元素的原理

    HashMap集合put元素的原理:(1)计算key的hashCode(2)将key的hashCode作为计算因子,通过哈希算法计算HashMap的数组下标index(3)如果index下标的数组元素 ...

  4. HashMap的底层实现原理

    HashMap的底层实现原理1,属性static final int MAX_CAPACITY = 1 << 30;//1073741824(十进制)0100000000000000000 ...

  5. HashMap底层实现及原理

    注意:文章的内容基于JDK1.7进行分析.1.8做的改动文章末尾进行讲解.       一.先来熟悉一下我们常用的HashMap: 1.HashSet和HashMap概述 对于HashSst及其子类而 ...

  6. Hashtable,HashMap和ConcurrentHashMap的原理及区别

    一.原理 Hashtable 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashM ...

  7. Map实现之HashMap(结构及原理)(转)

    java.util包中的集合类包含 Java 中某些最常用的类.最常用的集合类是 List 和 Map.List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构 ...

  8. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  9. Java集合:HashMap底层实现和原理(源码解析)

    Note:文章的内容基于JDK1.7进行分析.1.8做的改动文章末尾进行讲解. 一.先来熟悉一下我们常用的HashMap: 1.概述 HashMap基于Map接口实现,元素以键值对的方式存储,并且允许 ...

随机推荐

  1. react一写工具

    动画库:React-transition-group ui框架:Ant Design

  2. Viterbi(维特比)算法在CRF(条件随机场)中是如何起作用的?

    之前我们介绍过BERT+CRF来进行命名实体识别,并对其中的BERT和CRF的概念和作用做了相关的介绍,然对于CRF中的最优的标签序列的计算原理,我们只提到了维特比算法,并没有做进一步的解释,本文将对 ...

  3. APACHE HADOOP安装

    0.安装前准备 0.1 关闭防火墙 service iptables status service iptables stop 0.2 关闭Selinux 很多稀奇古怪的问题都是SELINUX导致的. ...

  4. C#异步案例一则

    场景 生产者和消费者队列, 生产者有多个, 消费者也有多个, 生产到消费需要异步. 下面用一个Asp.NetCore Web-API项目来模拟 创建两个API, 一个Get(), 一个Set(), G ...

  5. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

  6. 【日常错误】spring-boot配置文件读取不到

    最近在用spring-boot做项目时,遇到自定义的配置文件无法读取到的问题,通过在appcation.java类上定义@PropertySource(value = {"classpath ...

  7. 在可插拔settings的基础上加入类似中间件的设计

    在可插拔settings的基础上加入类似中间件的设计 settings可插拔设计可以看之前的文章 https://www.cnblogs.com/zx125/p/11735505.html 设计思路 ...

  8. re实战记录

    re实战记录 针对网页中的空格符 一般使用的.,但是它不能匹配\n,所以使用[\s\S]或者[\d\D]匹配所有字符 import re l1=r''' <div class="thu ...

  9. PHP函数include include_once require和require_once的区别

    了解下include.include_once.require和require_once这4个函数: include函数:会将指定的文件读入并且执行里面的程序: require函数:会将目标文件的内容 ...

  10. 小白学 Python 爬虫(13):urllib 基础使用(三)

    人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...