ConcurrentHashMap和HashMap的实现有很大的相似性,建议先看HashMap源码,再来理解ConcurrentHashMap。
这里仅展示几个关键的属性
1 // ConcurrentHashMap核心数组
2 transient volatile Node<K,V>[] table;
3
4 // 扩容时才会用的一个临时数组
5 private transient volatile Node<K,V>[] nextTable;
6
7 /**
8 * table初始化和resize控制字段
9 * 负数表示table正在初始化或resize。-1表示正在初始化,-N表示有N-1个线程正在resize操作
10 * 当table为null的时候,保存初始化表的大小以用于创建时使用,或者直接采用默认值0
11 * table初始化之后,保存下一次扩容的的大小,跟HashMap的threshold = loadFactor*capacity作用相同
12 */
13 private transient volatile int sizeCtl;
14
15 // resize的时候下一个需要处理的元素下标为index=transferIndex-1
16 private transient volatile int transferIndex;
17
18 // 通过CAS无锁更新,ConcurrentHashMap元素总数,但不是准确值
19 // 因为多个线程同时更新会导致部分线程更新失败,失败时会将元素数目变化存储在counterCells中
20 private transient volatile long baseCount;
21
22 // resize或者创建CounterCells时的一个标志位
23 private transient volatile int cellsBusy;
24
25 // 用于存储元素变动
26 private transient volatile CounterCell[] counterCells;
Unsafe.compareAndSwapXXX方法是sun.misc.Unsafe类中的方法,因为在ConcurrentHashMap中大量使用了这些方法。其声明如下:
public final native boolean compareAndSwapXXX(type1 object, type2 offset, type4 expect, type5 update);
object为待修改的对象,offset为偏移量(数组可以理解为下标),expect为期望值,update为更新值。这个方法执行的逻辑伪代码如下:
1 if (object[offset].value equal expect) {
2 object[offset].value = update;
3 return true;
4 } else {
5 return false
6 }
object[offset].value 等于expect更新value值并返回true,否则不更新并且返回false。之所以不更新是因为多线程执行时有其它线程已经修改该值,expect已经不是最新的值,如果强行修改必然会覆盖之前的修改,造成脏数据。
CAS方法都是native方法,可以保证原子性,并且效率比synchronized高。
1 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
2 int hash = spread(key.hashCode());
3
4 static final int spread(int h) {
5 return (h ^ (h >>> 16)) & HASH_BITS;
6 }
上面源码为计算hash算法,h ^ (h >>> 16)在计算hash的时候key.hashCode()的高位也参与运算,这部分跟HashMap计算方法一致,不同的是h ^ (h >>> 16)计算结果“与”上 0x7fffffff,从而保证结果一定为正整数。获得hash之后,通过hash & (n -1)计算下标。
ConcurrentHashMap中的元素节点总结一下有这么几种可能:
(1) null 暂无元素
(2) Node<K, V> 普通节点,可以组成单向链表,hash > 0
(3) TreeBin<K,V> 红黑树节点,TreeBin是对TreeNode的封装,其hash为TREEBIN = -2。
HashMap和ConcurrentHashMap的TreeNode实现并不相同。
在HashMap中TreeNode封装了红黑树所有的操作方法,而ConcurrentHashMap中红黑树操作的方法都封装在TreeBin中,TreeBin相当于一个红黑树容器,容器中的红黑树节点为TreeNode。
HashMap可以直接在tab[i]存入TreeNode,而ConCurrentHashMap只能在tab[i]存入TreeBin。
(4) ForwardingNode<K,V> key和value都为null的一个特殊节点,用于resize操作填充已经完成迁移操作的节点。FrowardingNode的hash在初始化的时候被置成MOVED = -1
在resize过程中当发现tab[i]上是ForwardingNode的时候(通过hash判断)就可知tab[i]已经迁移完了,直接跳过该节点去处理其它节点。
ConcurrentHashMap禁止node的key或value为null或许跟该节点的存在也是有一定关系的。
(5)ReservationNode<K,V>只在compute和computeIfAbsent中使用,其hash为RESERVED = -3
从上面的总结可以看出普通节点hash为正整数是有意义的,hash > 0是判断该节点是否为链表节点(普通节点)的一个重要依据。
1 // 获取tab[i]节点
2 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
3 return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
4 }
5
6 // compare and swap tab[i],期望值是c,tab[i].value == c ? tab[i] = v : return false
7 static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
8 return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
9 }
10
11 // 设置tab[i] = v
12 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
13 U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
14 }
1 public int size() {
2 long n = sumCount();
3 return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
4 }
5
6 final long sumCount() {
7 CounterCell[] as = counterCells; CounterCell a;
8 long sum = baseCount;
9 // 除了baseCount以外,部分元素变化存储在counterCells数组中
10 if (as != null) {
11 // 遍历数组累加获得结果
12 for (int i = 0; i < as.length; ++i) {
13 if ((a = as[i]) != null)
14 sum += a.value;
15 }
16 }
17 return sum;
18 }
ConcurrentHashMap中baseCount用于保存tab中元素总数,但是并不准确,因为多线程同时增删改,会导致baseCount修改失败,此时会将元素变动存储于counterCells数组内。
当需要统计当前的size的时候,除了要统计baseCount之外,还需要统计counterCells中的元素变化。
值得一提的是即使如此,统计出来的依旧不是当前tab中元素的准确值,在多线程环境下统计前后并不能stop the world暂停线程操作,因此无法保证准确性。
1 public V put(K key, V value) {
2 // 核心是调用putVal方法
3 return putVal(key, value, false);
4 }
5
6 public V putIfAbsent(K key, V value) {
7 // 如果key存在就不更新value
8 return putVal(key, value, true);
9 }
10
11 /** Implementation for put and putIfAbsent */
12 final V putVal(K key, V value, boolean onlyIfAbsent) {
13 // key或value 为null都是不允许的,因为Forwarding Node就是key和value都为null,是用作标志位的。
14 if (key == null || value == null) throw new NullPointerException();
15 // 根据key计算hash值,有了hash就可以计算下标了
16 int hash = spread(key.hashCode());
17 int binCount = 0;
18 // 可能需要初始化或扩容,因此一次未必能完成插入操作,所以添加上for循环
19 for (Node<K,V>[] tab = table;;) {
20 Node<K,V> f; int n, i, fh;
21 // 表还没有初始化,先初始化,lazily initialized
22 if (tab == null || (n = tab.length) == 0)
23 tab = initTable();
24 // 根据hash计算应该插入的index,该位置上还没有元素,则直接插入
25 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
26 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
27 break; // no lock when adding to empty bin
28 }
29 // static final int MOVED = -1; // hash for forwarding nodes
30 // 说明f为ForwardingNode,只有扩容的时候才会有ForwardingNode出现在tab中,因此可以断定该tab正在进行扩容
31 else if ((fh = f.hash) == MOVED)
32 // 协助扩容
33 tab = helpTransfer(tab, f);
34 else {
35 V oldVal = null;
36 // 节点上锁,hash值相同的节点组成的链表头结点
37 synchronized (f) {
38 if (tabAt(tab, i) == f) {
39 if (fh >= 0) { // 是链表节点
40 binCount = 1;
41 for (Node<K,V> e = f;; ++binCount) {
42 K ek;
43 // 遍历链表查找是否包含该元素
44 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
45 oldVal = e.val; // 保存旧的值用于当做返回值
46 if (!onlyIfAbsent)
47 e.val = value; // 替换旧的值为新值
48 break;
49 }
50 Node<K,V> pred = e;
51 if ((e = e.next) == null) {
52 // 遍历链表,如果一直没找到,则新建一个Node放到链表结尾
53 pred.next = new Node<K,V>(hash, key, value, null);
54 break;
55 }
56 }
57 }
58 else if (f instanceof TreeBin) { // 是红黑树节点
59 Node<K,V> p;
60 binCount = 2;
61 // 去红黑树查找该元素,如果没找到就添加,找到了就返回该节点
62 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
63 // 保存旧的value用于返回
64 oldVal = p.val;
65 if (!onlyIfAbsent)
66 p.val = value; // 替换旧的值
67 }
68 }
69 }
70 }
71 if (binCount != 0) {
72 if (binCount >= TREEIFY_THRESHOLD)
73 // 链表长度超过阈值(默认为8),则需要将链表转为一棵红黑树
74 treeifyBin(tab, i);
75 if (oldVal != null)
76 // 如果只是替换,并未带来节点的增加则直接返回旧的value即可
77 return oldVal;
78 break;
79 }
80 }
81 }
82 // 元素总数加1,并且判断是否需要扩容
83 addCount(1L, binCount);