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日,下午。

posted @ 2019-09-18 14:45  developer_chan  阅读(2029)  评论(0编辑  收藏  举报