• ConcurrentHashMap源码分析


    前言:ConcurrentHashMap是HashMap的线程安全版本,内部使用了数组+链表+红黑树的结构来存储数据,相对于同样线程安全的Hashtable来说,它在效率方面有很大的提升,因此多线程环境下更多的是使用ConcurrentHashMap,因此有必要对其原理进行分析。

    注:本文jdk源码版本为jdk1.8.0_172


    1.ConcurrentHashMap介绍

    ConcurrentHashMap是HashMap的线程安全版本,底层数据结构为数组+链表+红黑树,默认容量16,线程同步,不允许[key,value]为null。

    1 public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    2     implements ConcurrentMap<K,V>, Serializable

    构造函数:

     1 public ConcurrentHashMap() {
     2 }
     3 
     4  public ConcurrentHashMap(int initialCapacity) {
     5     if (initialCapacity < 0)
     6         throw new IllegalArgumentException();
     7     int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
     8                MAXIMUM_CAPACITY :
     9                tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    10     this.sizeCtl = cap;
    11 }
    12 
    13   public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    14     this.sizeCtl = DEFAULT_CAPACITY;
    15     putAll(m);
    16 }
    17 
    18  public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    19     this(initialCapacity, loadFactor, 1);
    20 }
    21 
    22     public ConcurrentHashMap(int initialCapacity,
    23                          float loadFactor, int concurrencyLevel) {
    24     if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
    25         throw new IllegalArgumentException();
    26     if (initialCapacity < concurrencyLevel)   // Use at least as many bins
    27         initialCapacity = concurrencyLevel;   // as estimated threads
    28     long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    29     int cap = (size >= (long)MAXIMUM_CAPACITY) ?
    30         MAXIMUM_CAPACITY : tableSizeFor((int)size);
    31     this.sizeCtl = cap;
    32 }

    分析:

    通过构造函数可以发现sizeCtl变量经常出现,该变量通过查看jdk源码注释可知该变量主要控制初始化或扩容:

    #1.-1,表示线程正在进行初始化操作。

    #2.-(1+nThreads),表示n个线程正在进行扩容。

    #3.0,默认值,后续在真正初始化的时候使用默认容量。

    #4.>0,初始化或扩容完成后下一次的扩容门槛。

    2.具体源码分析

    put操作:

     1 final V putVal(K key, V value, boolean onlyIfAbsent) {
     2         if (key == null || value == null) throw new NullPointerException();
     3         // 计算key的hash值
     4         int hash = spread(key.hashCode());
     5         // 用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
     6         int binCount = 0;
     7         // 进行自旋
     8         for (Node<K,V>[] tab = table;;) {
     9             Node<K,V> f; int n, i, fh;
    10             if (tab == null || (n = tab.length) == 0)
    11                 // table未初始化,则初始化
    12                 tab = initTable();
    13             // 如果该位置上的f为null,则说明第一次插入元素,则直接插入新的Node节点
    14             else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    15                 if (casTabAt(tab, i, null,
    16                              new Node<K,V>(hash, key, value, null)))
    17                     break;                   // no lock when adding to empty bin
    18             }
    19             // 如果检测到当前某个节点的hash值为MOVED,则表示正在进行数组扩张的数据复制阶段
    20             // 则当前线程与会参与复制,通过允许多线程复制的功能,减少数组的复制来带来的性能损失
    21             else if ((fh = f.hash) == MOVED)
    22                 tab = helpTransfer(tab, f);
    23             else {
    24                 V oldVal = null;
    25                 /**
    26                  * 到该分支表明该位置上有元素,采用synchronized方式加锁
    27                  * 如果是链表的话,则对链表进行遍历,找到key和key的hash值都一样的节点,进行替换
    28                  * 如果没有找到,则添加在链表最后面
    29                  * 如果是树的话,则添加到树中去
    30                  */
    31                 synchronized (f) {
    32                     // 再次取出要存储的位置元素,跟之前的数据进行比较,看是否进行了更改
    33                     if (tabAt(tab, i) == f) {
    34                         // 链表
    35                         if (fh >= 0) {
    36                             binCount = 1;
    37                             // 遍历链表
    38                             for (Node<K,V> e = f;; ++binCount) {
    39                                 K ek;
    40                                 // 元素的hash、key都相同,则进行替换和hashMap相同
    41                                 if (e.hash == hash &&
    42                                     ((ek = e.key) == key ||
    43                                      (ek != null && key.equals(ek)))) {
    44                                     oldVal = e.val;
    45                                     // 当使用putIfAbsent的时候,只有在这个key没有设置值时的候才设置
    46                                     if (!onlyIfAbsent)
    47                                         e.val = value;
    48                                     break;
    49                                 }
    50                                 Node<K,V> pred = e;
    51                                 // 不同key,hash值相同时,直接添加到链表尾即可
    52                                 if ((e = e.next) == null) {
    53                                     pred.next = new Node<K,V>(hash, key,
    54                                                               value, null);
    55                                     break;
    56                                 }
    57                             }
    58                         }
    59                         // 当前结点为红黑树
    60                         else if (f instanceof TreeBin) {
    61                             Node<K,V> p;
    62                             binCount = 2;
    63                             // 添加元素到树中去,表明树的当前结点存在值,则进行替换
    64                             if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
    65                                                            value)) != null) {
    66                                 oldVal = p.val;
    67                                 if (!onlyIfAbsent)
    68                                     p.val = value;
    69                             }
    70                         }
    71                     }
    72                 }
    73                 if (binCount != 0) {
    74                     // 当在同一个节点的数目大于等于8时,则进行扩容或者将数据转换成红黑树
    75                     // 注意,这里并不一定是直接转换成红黑树,有可能先进行扩容
    76                     if (binCount >= TREEIFY_THRESHOLD)
    77                         treeifyBin(tab, i);
    78                     if (oldVal != null)
    79                         return oldVal;
    80                     break;
    81                 }
    82             }
    83         }
    84         // 计数 binCount大于1(链表的长度)表示链表,binCount=2表示红黑树
    85         addCount(1L, binCount);
    86         return null;
    87     }

    分析:

    通过查看put操作的核心源码,整体逻辑还是比较清晰,有几个点需要注意:

    #1.在插入元素时,采用了自旋。

    #2.在插入元素的时候才会进行初始化。

    #3.在插入元素时,底层数据结构可能会转向红黑树。

    initTable:初始化函数

     1  private final Node<K,V>[] initTable() {
     2         Node<K,V>[] tab; int sc;
     3         while ((tab = table) == null || tab.length == 0) {
     4             // sizeCtl初始值为0,当小于0时,表示在别的线程初始化表或扩展表,当前线程只需要让出cpu时间片即可
     5             if ((sc = sizeCtl) < 0)
     6                 Thread.yield(); // lost initialization race; just spin
     7             // 将sc更新为-1,表示线程正在进行初始化操作
     8             else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
     9                 try {
    10                     if ((tab = table) == null || tab.length == 0) {
    11                         // 指定了大小就创建指定大小的Node数组,否则创建默认大小的Node数组
    12                         int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
    13                         @SuppressWarnings("unchecked")
    14                         Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
    15                         table = tab = nt;
    16                         sc = n - (n >>> 2);
    17                     }
    18                 } finally {
    19                     // 和上面逻辑对别可知sizeCtl的大小为数组长度的3/4
    20                     sizeCtl = sc;
    21                 }
    22                 break;
    23             }
    24         }
    25         return tab;
    26     }

    分析:

    在put操作时才进行初始化操作其实是懒加载的一种表现形式,并且初始化时,已考虑多线程的情况,默认容量为16

    当挂在链表上的元素大于等于8时,会通过treeifyBin方法来判断是否扩容或转换为一棵树。

    treeifyBin:

     1 private final void treeifyBin(Node<K,V>[] tab, int index) {
     2         Node<K,V> b; int n, sc;
     3         if (tab != null) {
     4             // 如果数组长度小于64则进行扩容
     5             if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
     6                 tryPresize(n << 1); 
     7             else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
     8                 // 将链表转换成树
     9                 synchronized (b) {
    10                     // 再次比较当前位置结点是否改变
    11                     if (tabAt(tab, index) == b) {
    12                         TreeNode<K,V> hd = null, tl = null; // hd:树的头(head)
    13                         for (Node<K,V> e = b; e != null; e = e.next) {
    14                             TreeNode<K,V> p =
    15                                 new TreeNode<K,V>(e.hash, e.key, e.val,
    16                                                   null, null);
    17                             // 链表转换成树后,头节点依然在相同位置
    18                             if ((p.prev = tl) == null)
    19                                 hd = p;
    20                             else
    21                                 tl.next = p;
    22                             tl = p;
    23                         }
    24                         setTabAt(tab, index, new TreeBin<K,V>(hd));
    25                     }
    26                 }
    27             }
    28         }
    29     }

    分析:

    从上述源码上看,当节点链表上的元素大于等于8时,并不是一定要将数据结构转换成树。而是要先判断数组的容量,如果数组长度小于64,会进行扩容(扩容为原来数组长度的一倍),否则才会转换成树。

    tryPresize:扩容函数,注意通过treeifyBin调用tryPresize时,入参已经扩大2倍

     1    /**
     2      * 扩容时大小总是2的N次方
     3      * 扩容这里可能有一点绕,用一个例子来走下流程
     4      * 假设原来数组长度为16(默认值),在调用tryPresize的时候size的值已经变成了32(16<<1),此时sizeCtl为12
     5      * 计算出c的值为64,注意扩容会在transfer中进行(前提数组已经初始化),每次扩大2倍,由于数组长度基数为2的N次方,所以最终的数组长度也是2的N次方。
     6      * 注意c的值是用来控制循环退出的,条件c<=sc(sizeCtl)。
     7      *           数组长度   sizeCtl
     8      *第一次扩容:  32        28
     9      *第二次扩容:  64        48
    10      *第三次扩容:  128       96   此时c(64)<sc(96) 此时退出扩容
    11      */
    12 private final void tryPresize(int size) {
    13         // 通过tableSizeFor计算扩容退出控制量标志,容量大小总是2的N次方
    14         int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
    15             tableSizeFor(size + (size >>> 1) + 1);
    16         int sc;
    17         while ((sc = sizeCtl) >= 0) {
    18             Node<K,V>[] tab = table; int n;
    19             // 初始化
    20             // 如果tab未初始化,则初始化一个大小为sizeCtl和c中较大的数组
    21             // 初始化是将sizeCtl设置为-1,完成之后将其设置为数组长度的3/4
    22             // 在此进行初始化,主要是因为如果直接调用putAll方法进行元素添加时,table还未初始化,所以这里需要判断table是否进行了初始化
    23             if (tab == null || (n = tab.length) == 0) {
    24                 n = (sc > c) ? sc : c;
    25                 // 初始化tab的时候,把sizeCtl设置为-1,通过CAS
    26                 if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
    27                     try {
    28                         if (table == tab) {
    29                             @SuppressWarnings("unchecked")
    30                             Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
    31                             table = nt;
    32                             sc = n - (n >>> 2);
    33                         }
    34                     } finally {
    35                         sizeCtl = sc;
    36                     }
    37                 }
    38             }
    39             // 一直扩容到c小于等于sizeCtl或者数组长度大于最大长度的时候,退出扩容
    40             else if (c <= sc || n >= MAXIMUM_CAPACITY)
    41                 break;
    42             else if (tab == table) {
    43                 int rs = resizeStamp(n);
    44                 // 如果正在扩容,则帮助扩容
    45                 // 否则的话,开始新的扩容
    46                 // 在transfer操作,将第一个参数的table元素,移到第二个元素的table去,
    47                 // 虽然此时第二个参数设置的是null,但是在transfer方法中,第二个参数为null的时候,会创建一个两倍大小的table
    48                 // sc小于0表示有线程在进行操作
    49                 if (sc < 0) {
    50                     Node<K,V>[] nt;
    51                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
    52                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
    53                         transferIndex <= 0)
    54                         break;
    55                     // 将线程数加一,该线程将进行transfer,在transfer的时候,sc表示transfer工作线程数
    56                     if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
    57                         transfer(tab, nt);
    58                 }
    59                 // 没有初始化或扩容,直接进行扩容
    60                 else if (U.compareAndSwapInt(this, SIZECTL, sc,
    61                                              (rs << RESIZE_STAMP_SHIFT) + 2))
    62                     transfer(tab, null);
    63             }
    64         }
    65     }

    分析:

    扩容时稍微有一点绕,但上面注释给出了一个例子,理解该例子应该就可以理解扩容,特别要注意源码中的c值,可以看做是扩容控制值,通过该值来终止扩容函数。

    transfer:数组扩容函数

      1  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
      2         int n = tab.length, stride;
      3         // 确定线程负责数组大小的范围
      4         if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
      5             stride = MIN_TRANSFER_STRIDE; // subdivide range
      6         // 扩容后数组长度为原来的两倍
      7         if (nextTab == null) {            // initiating
      8             try {
      9                 @SuppressWarnings("unchecked")
     10                 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
     11                 nextTab = nt;
     12             } catch (Throwable ex) {      // try to cope with OOME
     13                 sizeCtl = Integer.MAX_VALUE;
     14                 return;
     15             }
     16             nextTable = nextTab;
     17             transferIndex = n;
     18         }
     19         int nextn = nextTab.length;
     20         /**
     21          * 创建一个fwd结点,用来控制并发,当一个结点为空或者已经被转移之后,就设置为fwd结点
     22          * 这是一个空的标志节点
     23          */
     24         ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
     25         // 是否继续向前查找的标志位
     26         boolean advance = true;
     27         boolean finishing = false; // to ensure sweep before committing nextTab
     28         for (int i = 0, bound = 0;;) {
     29             Node<K,V> f; int fh;
     30             while (advance) {
     31                 int nextIndex, nextBound;
     32                 if (--i >= bound || finishing)
     33                     advance = false;
     34                 else if ((nextIndex = transferIndex) <= 0) {
     35                     i = -1;
     36                     advance = false;
     37                 }
     38                 else if (U.compareAndSwapInt
     39                          (this, TRANSFERINDEX, nextIndex,
     40                           nextBound = (nextIndex > stride ?
     41                                        nextIndex - stride : 0))) {
     42                     bound = nextBound;
     43                     i = nextIndex - 1;
     44                     advance = false;
     45                 }
     46             }
     47             if (i < 0 || i >= n || i + n >= nextn) {
     48                 int sc;
     49                 // 数据迁移完成,替换旧桶数据
     50                 if (finishing) {
     51                     nextTable = null;
     52                     table = nextTab;
     53                     // 设置sizeCtl为扩容后的0.75
     54                     sizeCtl = (n << 1) - (n >>> 1);
     55                     return;
     56                 }
     57                 // 扩容完成,将扩容线程数-1
     58                 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
     59                     if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
     60                         return;
     61                     // finishing和advance设置为true,重新走到上面if条件,再次检查是否迁移完
     62                     // 通过fh=f.hash==MOVED进行判断
     63                     finishing = advance = true;
     64                     i = n; // recheck before commit
     65                 }
     66             }
     67             // 如果桶中无数据,则放入fwd标记,表示该位置已迁移
     68             else if ((f = tabAt(tab, i)) == null)
     69                 advance = casTabAt(tab, i, null, fwd);
     70             // 如果桶中第一个元素的hash值为MOVED,说明该节点为fwd节点,详情看fwd节点的构造函数
     71             // 说明该位置已经被迁移
     72             else if ((fh = f.hash) == MOVED)
     73                 advance = true; // already processed
     74             else {
     75                 // 加锁迁移元素
     76                 synchronized (f) {
     77                     // 再次判断桶中第一个元素是否有过修改
     78                     if (tabAt(tab, i) == f) {
     79                         /**
     80                          * 把一个链表划分成两个链表
     81                          * 规则是桶中各元素的hash值与桶大小n进行与操作
     82                          * 等于0的放到低位链表(low)中,等于1的放到高位链表(high)中
     83                          * 其中低位链表迁移到新桶的位置是相对旧桶不变的
     84                          * 高位链表迁移到新桶的位置正好是其在旧桶位置上加n,这里在HashMap(jdk1.8中)分析过。
     85                          * 这就是为什么扩容时,容量变成原来两倍的原因
     86                          */
     87                         Node<K,V> ln, hn; // ln:low节点 hn:height节点
     88                         // 链表的节点hash值大于0,TreeBin的hash值为-2
     89                         if (fh >= 0) {
     90                             // 首先计算出当前结点的位置
     91                             int runBit = fh & n;
     92                             Node<K,V> lastRun = f;
     93                             for (Node<K,V> p = f.next; p != null; p = p.next) {
     94                                 int b = p.hash & n;
     95                                 // 同一节点下hashCode可能是不同的,这样才会有hash分布
     96                                 // 更新runBit的值,找出与f不同的节点
     97                                 // 这里一直要找到链表尾,但是lastRun不一定是尾节点,也就是找到最后一段相同的
     98                                 // 因为是链表,当位置相同,直接就带过去了,避免没必要的循环
     99                                 if (b != runBit) {
    100                                     runBit = b;
    101                                     lastRun = p;
    102                                 }
    103                             }
    104                             // 设置低位节点
    105                             if (runBit == 0) {
    106                                 ln = lastRun;
    107                                 hn = null;
    108                             }
    109                             // 设置高位节点
    110                             else {
    111                                 hn = lastRun;
    112                                 ln = null;
    113                             }
    114                             // 生成两条链表,直接拼接
    115                             // 找到不等于lastRun的节点,进行拼接,不是倒序,这里就是进行一个拼接,因为把hash值相同的链从lastRun带过来了
    116                             for (Node<K,V> p = f; p != lastRun; p = p.next) {
    117                                 int ph = p.hash; K pk = p.key; V pv = p.val;
    118                                 if ((ph & n) == 0)
    119                                     ln = new Node<K,V>(ph, pk, pv, ln);
    120                                 else
    121                                     hn = new Node<K,V>(ph, pk, pv, hn);
    122                             }
    123                             // 这里设置和hashMap类似,在相应点上设置节点即可
    124                             setTabAt(nextTab, i, ln);
    125                             setTabAt(nextTab, i + n, hn);
    126                             // 在旧的链表位置上设置占位符,标记已迁移完成
    127                             setTabAt(tab, i, fwd);
    128                             advance = true;
    129                         }
    130                         /**
    131                          * 结点是树的情况
    132                          * 和链表相同,分成两颗树,根据hash&n为0的放在低位树,为1的放在高位树
    133                          */
    134                         else if (f instanceof TreeBin) {
    135                             TreeBin<K,V> t = (TreeBin<K,V>)f;
    136                             TreeNode<K,V> lo = null, loTail = null;
    137                             TreeNode<K,V> hi = null, hiTail = null;
    138                             int lc = 0, hc = 0;
    139                             // 遍历整棵树,根据hash&n是否为0进行划分
    140                             for (Node<K,V> e = t.first; e != null; e = e.next) {
    141                                 int h = e.hash;
    142                                 TreeNode<K,V> p = new TreeNode<K,V>
    143                                     (h, e.key, e.val, null, null);
    144                                 if ((h & n) == 0) {
    145                                     if ((p.prev = loTail) == null)
    146                                         lo = p;
    147                                     else
    148                                         loTail.next = p;
    149                                     loTail = p;
    150                                     ++lc;
    151                                 }
    152                                 else {
    153                                     if ((p.prev = hiTail) == null)
    154                                         hi = p;
    155                                     else
    156                                         hiTail.next = p;
    157                                     hiTail = p;
    158                                     ++hc;
    159                                 }
    160                             }
    161                             // 复制完树结点之后,如果树的节点小于等于6时,就转回链表
    162                             ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
    163                                 (hc != 0) ? new TreeBin<K,V>(lo) : t;
    164                             hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
    165                                 (lc != 0) ? new TreeBin<K,V>(hi) : t;
    166                             // 低位树的位置不变
    167                             setTabAt(nextTab, i, ln);
    168                             // 高位树的位置在原来位置上加n
    169                             setTabAt(nextTab, i + n, hn);
    170                             // 标记该位置已经进迁移
    171                             setTabAt(tab, i, fwd);
    172                             // 继续循环,执行--i操作
    173                             advance = true;
    174                         }
    175                     }
    176                 }
    177             }
    178         }
    179     }

    分析:

    扩容函数中对于中间有段求i的值不是特别明白,其他流程还是比较清楚的,和HashMap的扩容有点类似,链表分成两段进行处理,通过hash&n是否等于0进行划分,迁移是从靠后的桶开始的(具体就在中间那段求i的值处),在迁移过程中锁住了当前桶,还是采用了分段锁的思想。需注意:#1.针对树节点,如果扩容后树节点上的元素总数小于等于6,则会退化成链表;#2.在链表拆分后进行组合时并不一定是倒序

    在put操作中还有一个帮助扩容的函数:helpTransfer

     1  // 线程添加元素时发现正在扩容且当前元素所在的桶已经迁移完成,则协助迁移其他桶的元素
     2     final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
     3         Node<K,V>[] nextTab; int sc;
     4         // 如果桶数组不为空,并且当前桶第一个元素为fwd类型,且nexttable不为空
     5         // 说明当前桶已经迁移完毕,可以去帮助迁移其他的桶的元素了
     6         if (tab != null && (f instanceof ForwardingNode) &&
     7             (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
     8             int rs = resizeStamp(tab.length);
     9            // sizeCtl<0,说明正在扩容
    10             while (nextTab == nextTable && table == tab &&
    11                    (sc = sizeCtl) < 0) {
    12                 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
    13                     sc == rs + MAX_RESIZERS || transferIndex <= 0)
    14                     break;
    15                 // 扩容线程数加1
    16                 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
    17                     // 当前线程帮忙迁移元素
    18                     transfer(tab, nextTab);
    19                     break;
    20                 }
    21             }
    22             return nextTab;
    23         }
    24         return table;
    25     }

    分析:

    只有当前桶元素迁移完成了才能去协助迁移其他桶的元素。

    接下来看addCount函数,该函数在put操作后会判断是否需要扩容,如果达到扩容门槛,则进行扩容或协助扩容。

     1  private final void addCount(long x, int check) {
     2         CounterCell[] as; long b, s;
     3         // 如果计数盒子不为空,或者修改baseCount失败
     4         if ((as = counterCells) != null ||
     5             !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
     6             CounterCell a; long v; int m;
     7             boolean uncontended = true;
     8             // 如果as为空,或者长度为0,或者当前线程所在的段为null,或者在当前线程的段上加数量失败
     9             if (as == null || (m = as.length - 1) < 0 ||
    10                 (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
    11                 !(uncontended =
    12                   U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
    13                 // 这里对counterCells扩容,减少多线程hash到同一个段的频率
    14                 fullAddCount(x, uncontended);
    15                 return;
    16             }
    17             if (check <= 1)
    18                 return;
    19             // 计算元素个数
    20             s = sumCount();
    21         }
    22         if (check >= 0) {
    23             Node<K,V>[] tab, nt; int n, sc;
    24             // 如果元素个数达到了扩容门槛,则进行扩容
    25             // sizeCtl即为扩容门槛,它为容量的0.75倍
    26             while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
    27                    (n = tab.length) < MAXIMUM_CAPACITY) {
    28                 // rs是扩容的一个邮戳标识
    29                 int rs = resizeStamp(n);
    30                 // sc小于0,表明正在扩容
    31                 if (sc < 0) {
    32                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
    33                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
    34                         transferIndex <= 0)
    35                         // 扩容完成,退出循环
    36                         break;
    37                     // 扩容未完成,将当前线程加入迁移元素中,并把扩容线程数加1
    38                     if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
    39                         transfer(tab, nt);
    40                 }
    41                 else if (U.compareAndSwapInt(this, SIZECTL, sc,
    42                                              (rs << RESIZE_STAMP_SHIFT) + 2))
    43                     // 进行元素迁移
    44                     transfer(tab, null);
    45                 // 重新计算元素个数
    46                 s = sumCount();
    47             }
    48         }
    49     }

    分析:

    该函数的主要作用就是将元素个数加1,并且判断是否需要进行扩容。目前对该函数的详细逻辑不是特别清楚,后续再来进行分析。

    一个put操作涉及的内容太多了,还需深入理解,下面来看get操作:

     1 public V get(Object key) {
     2         Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
     3         // 计算hash
     4         int h = spread(key.hashCode());
     5         // 如果对应位置上有元素
     6         if ((tab = table) != null && (n = tab.length) > 0 &&
     7             (e = tabAt(tab, (n - 1) & h)) != null) {
     8             // 如果第一个元素就是要找的元素,则直接返回
     9             if ((eh = e.hash) == h) {
    10                 if ((ek = e.key) == key || (ek != null && key.equals(ek)))
    11                     return e.val;
    12             }
    13             // 如果hash小于0,则说明是树或正在扩容,则使用find寻找元素,find根据Node的不同子类实现方式不同
    14             else if (eh < 0)
    15                 return (p = e.find(h, key)) != null ? p.val : null;
    16             // 遍历整个链表寻找元素
    17             while ((e = e.next) != null) {
    18                 if (e.hash == h &&
    19                     ((ek = e.key) == key || (ek != null && key.equals(ek))))
    20                     return e.val;
    21             }
    22         }
    23         return null;
    24     }

    分析:

    get操作整体来说逻辑清楚明了,与HashMap类似,但是要注意hash值小于0的时候,其寻找元素的方式有所不同,并且整个获取元素的过程是没有加锁的。

    接下来看remove操作:

     1 final V replaceNode(Object key, V value, Object cv) {
     2         // 计算hash值
     3         int hash = spread(key.hashCode());
     4         // 进行自旋操作
     5         for (Node<K,V>[] tab = table;;) {
     6             Node<K,V> f; int n, i, fh;
     7             // 如果tab为空,或者key所在的位置上没有元素,则直接终止自旋
     8             if (tab == null || (n = tab.length) == 0 ||
     9                 (f = tabAt(tab, i = (n - 1) & hash)) == null)
    10                 break;
    11             // 正在扩容,则协助其扩容
    12             else if ((fh = f.hash) == MOVED)
    13                 tab = helpTransfer(tab, f);
    14             else {
    15                 V oldVal = null;
    16                 // 标记是否处理过
    17                 boolean validated = false;
    18                 // 加锁
    19                 synchronized (f) {
    20                     // 再次验证当前位置上的元素是否被修改过
    21                     if (tabAt(tab, i) == f) {
    22                         // 链表
    23                         if (fh >= 0) {
    24                             validated = true;
    25                             // 遍历链表,寻找节点
    26                             for (Node<K,V> e = f, pred = null;;) {
    27                                 K ek;
    28                                 if (e.hash == hash &&
    29                                     ((ek = e.key) == key ||
    30                                      (ek != null && key.equals(ek)))) {
    31                                     // 找到目标元素
    32                                     V ev = e.val;
    33                                     if (cv == null || cv == ev ||
    34                                         (ev != null && cv.equals(ev))) {
    35                                         oldVal = ev;
    36                                         // 如果value不为空,则替换旧值
    37                                         if (value != null)
    38                                             e.val = value;
    39                                         else if (pred != null)
    40                                             // 前置节点不为空,删除当前节点
    41                                             pred.next = e.next;
    42                                         else
    43                                             // 如果前置节点为空,则说明是桶中第一个元素,则删除即可
    44                                             setTabAt(tab, i, e.next);
    45                                     }
    46                                     break;
    47                                 }
    48                                 // 更新前置节点
    49                                 pred = e;
    50                                 // 遍历到链表尾还未找打元素,则跳出循环
    51                                 if ((e = e.next) == null)
    52                                     break;
    53                             }
    54                         }
    55                         // 节点是树
    56                         else if (f instanceof TreeBin) {
    57                             validated = true;
    58                             TreeBin<K,V> t = (TreeBin<K,V>)f;
    59                             TreeNode<K,V> r, p;
    60                             // 遍历树找到目标节点
    61                             if ((r = t.root) != null &&
    62                                 (p = r.findTreeNode(hash, key, null)) != null) {
    63                                 V pv = p.val;
    64                                 if (cv == null || cv == pv ||
    65                                     (pv != null && cv.equals(pv))) {
    66                                     oldVal = pv;
    67                                     if (value != null)
    68                                         // 替换旧值
    69                                         p.val = value;
    70                                     else if (t.removeTreeNode(p))
    71                                         // 当removeTreeNode返回true表示树的元素个数较少,则退化成链表
    72                                         setTabAt(tab, i, untreeify(t.first));
    73                                 }
    74                             }
    75                         }
    76                     }
    77                 }
    78                 // 如果处理过
    79                 if (validated) {
    80                     // 找到了元素,返回其旧值
    81                     if (oldVal != null) {
    82                         // 如果要替换的值为空,则将元素个数减1
    83                         if (value == null)
    84                             addCount(-1L, -1);
    85                         return oldVal;
    86                     }
    87                     break;
    88                 }
    89             }
    90         }
    91         return null;
    92     }

    分析:

    利用自旋删除元素,整体流程清晰,根据链表或树进行相应操作,注意如果删除过程中正在进行扩容,需要协助其扩容后再进行删除。

    size函数:获取元素个数

     1 public int size() {
     2     // 调用sumCount计算元素个数
     3     long n = sumCount();
     4     return ((n < 0L) ? 0 :
     5             (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
     6             (int)n);
     7 }
     8 
     9    final long sumCount() {
    10     // 计算CounterCell所有段以及baseCount的数量之和
    11     CounterCell[] as = counterCells; CounterCell a;
    12     long sum = baseCount;
    13     if (as != null) {
    14         for (int i = 0; i < as.length; ++i) {
    15             if ((a = as[i]) != null)
    16                 sum += a.value;
    17         }
    18     }
    19     return sum;
    20 }

    分析:

    元素的个数会计算CounterCell所有段和baseCount之和,并且该函数是没有加锁的。

    3.总结

    ConcurrentHashMap的源码分析真不容易,代码量非常的大,其实有的地方目前还没弄懂,需后续反复阅读。

    #1.ConcurrentHashMap是HashMap的线程安全版本。

    #2.ConcurrentHashMap底层数据结构为数组+链表+红黑树,默认容量为16,不允许[key,value]为null。

    #3.ConcurrentHashMap内部采用的锁有synchronized、CAS、自旋锁、分段锁、volatile。

    #4.通过sizeCtl变量来控制扩容、初始化等操作。

    #5.查询操作不加锁,因此ConcurrentHashMap不是强一致性

    ConcurrentHashMap未完待续!!!


    by Shawn Chen,2019.09.18日,下午。

  • 相关阅读:
    BZOJ-4008: [HNOI2015]亚瑟王 (概率期望DP)
    BZOJ-4832: [Lydsy2017年4月月赛]抵制克苏恩 (概率期望DP)
    BZOJ-1415: [Noi2005]聪聪和可可 (期望DP)
    BZOJ2425 [HAOI2010]计数
    BZOJ2424 [HAOI2010]订货
    BZOJ2423 [HAOI2010]最长公共子序列
    BZOJ2299 [HAOI2011]向量
    BZOJ2298 [HAOI2011]problem a
    BZOJ1040 [ZJOI2008]骑士
    BZOJ一天提交(AC) 51纪念
  • 原文地址:https://www.cnblogs.com/developer_chan/p/11527120.html
Copyright © 2020-2023  润新知