并发编程学习笔记(二十七、ConcurrentHashMap,Java8 ConcurrentHashMap)

目录:

  • 属性及内部类
  • 构造函数
  • 核心方法

属性及内部类

1、属性:

其实说到属性最终要的也无法就是一个hash数组桶,其它核心的属性都是大同小异。

1 transient volatile Node<K,V>[] table;

2、Node:ConcurrentHashMap存储数据的基本单元。

 1 static class Node<K,V> implements Map.Entry<K,V> {
 2     final int hash;
 3     final K key;
 4     volatile V val;
 5     volatile Node<K,V> next;
 6 
 7     Node(int hash, K key, V val, Node<K,V> next) {
 8         this.hash = hash;
 9         this.key = key;
10         this.val = val;
11         this.next = next;
12     }
13 
14     public final K getKey()       { return key; }
15     public final V getValue()     { return val; }
16     public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
17     public final String toString(){ return key + "=" + val; }
18     /** ConcurrentHashMap不允许更新value **/
19     public final V setValue(V value) {
20         throw new UnsupportedOperationException();
21     }
22 
23     public final boolean equals(Object o) {
24         Object k, v, u; Map.Entry<?,?> e;
25         return ((o instanceof Map.Entry) &&
26                 (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
27                 (v = e.getValue()) != null &&
28                 (k == key || k.equals(key)) &&
29                 (v == (u = val) || v.equals(u)));
30     }
31 
32     /**
33      * 用于map中的get()方法,子类重写
34      */
35     Node<K,V> find(int h, Object k) {
36         Node<K,V> e = this;
37         if (k != null) {
38             do {
39                 K ek;
40                 if (e.hash == h &&
41                     ((ek = e.key) == k || (ek != null && k.equals(ek))))
42                     return e;
43             } while ((e = e.next) != null);
44         }
45         return null;
46     }
47 }

Node非常简单,一眼就能看出其底层用链表实现,但只允许查找数据,不允许修改

3、TreeNode:继承自Node,但内部使用的数据结构是红黑树,当链表长度大于8时就会从Node转换成TreeNode

4、TreeBin:存储属性结构的容器,也就是TreeNode的容器提供了一些红黑树的条件和锁的控制

5、ForwardingNode:支持扩容操作,将已处理的节点和空节点置为ForwardingNode,并发处理时多个线程经过ForwardingNode就表示已经遍历过了

构造函数

Java8的构造和7类似,且ConcurrentHashMap和HashMap的无参构造也同样使用的是懒加载。

1 public ConcurrentHashMap() {}

核心方法

1、put()、putVal():

1 public V put(K key, V value) {
2     return putVal(key, value, false);
3 }
 1 final V putVal(K key, V value, boolean onlyIfAbsent) {
 2     if (key == null || value == null) throw new NullPointerException();
 3     // 两次hash,减少hash冲突,可让其均匀分布
 4     int hash = spread(key.hashCode());
 5     int binCount = 0;
 6     // 遍历table
 7     for (Node<K,V>[] tab = table;;) {
 8         Node<K,V> f; int n, i, fh;
 9         if (tab == null || (n = tab.length) == 0)
10             // 和HashMap同样的使用懒加载的方式,在用的时候再初始化
11             tab = initTable();
12         // 如果位置i没有数据
13         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
14             // 使用CAS的方式插入
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         // 如果在进行扩容,则帮助其进行扩容操作
20         else if ((fh = f.hash) == MOVED)
21             tab = helpTransfer(tab, f);
22         // 若以上条件都不满足,则就是要进行加锁操作
23         // 也就是存在hash冲突是,需要锁住链表或红黑树的头节点
24         else {
25             V oldVal = null;
26             synchronized (f) {
27                 if (tabAt(tab, i) == f) {
28                     // 标识该节点是链表结构
29                     if (fh >= 0) {
30                         binCount = 1;
31                         for (Node<K,V> e = f;; ++binCount) {
32                             K ek;
33                             // 若是相同的key则会覆盖老的value
34                             if (e.hash == hash &&
35                                 ((ek = e.key) == key ||
36                                  (ek != null && key.equals(ek)))) {
37                                 oldVal = e.val;
38                                 if (!onlyIfAbsent)
39                                     e.val = value;
40                                 break;
41                             }
42                             // 插入到链表的尾部
43                             Node<K,V> pred = e;
44                             if ((e = e.next) == null) {
45                                 pred.next = new Node<K,V>(hash, key,
46                                                           value, null);
47                                 break;
48                             }
49                         }
50                     }
51                     // 若是红黑树结构
52                     else if (f instanceof TreeBin) {
53                         Node<K,V> p;
54                         binCount = 2;
55                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
56                                                        value)) != null) {
57                             oldVal = p.val;
58                             if (!onlyIfAbsent)
59                                 p.val = value;
60                         }
61                     }
62                 }
63             }
64             if (binCount != 0) {
65                 // 若链表长度大于8时,则会转换成红黑树
66                 if (binCount >= TREEIFY_THRESHOLD)
67                     treeifyBin(tab, i);
68                 if (oldVal != null)
69                     return oldVal;
70                 break;
71             }
72         }
73     }
74     // 统计size,并检查是否需要扩容
75     addCount(1L, binCount);
76     return null;
77 }

putVal的过程其实很清晰,我来简单的总结下:

  • 如果还没有初始化则初始化。
  • 如果没有hash冲突则直接CAS插入。
  • 如果还在进行扩容则先进行扩容。
  • 如果存在hash冲突,就先加锁来保证线程安全。
    • 链表就遍历插入到尾端。
    • 红黑树就按照红黑树的结构插入。
  • 如果链表长度大于阀值8,则先转换成红黑树,在进入一次循环。
  • 如果添加成功后调用addCount()方法统计size,并检查是否需要扩容。

2、initTable():初始化ConcurrentHashMap存储槽。

 1 private final Node<K,V>[] initTable() {
 2     Node<K,V>[] tab; int sc;、
 3     while ((tab = table) == null || tab.length == 0) {
 4         if ((sc = sizeCtl) < 0)
 5             Thread.yield(); // lost initialization race; just spin
 6         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
 7             try {
 8                 if ((tab = table) == null || tab.length == 0) {
 9                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
10                     @SuppressWarnings("unchecked")
11                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
12                     table = tab = nt;
13                     sc = n - (n >>> 2);
14                 }
15             } finally {
16                 sizeCtl = sc;
17             }
18             break;
19         }
20     }
21     return tab;
22 }

这段代码虽然看起来简单,但你必须要知道sizeCtl的含义后才能弄明白。

1 /**
2  * 用于控制table初始化和resize的参数。
3  * -1:表示正在发生初始化
4  * 一个很小的负数(非固定数字):标识table正在调整大小
5  * 0:代表默认状态
6  * 16 * 2 ^ n * 0.75:下一次扩容的阀值
7  */
8 private transient volatile int sizeCtl;

说完了sizeCtl后,你就能很轻松的看懂initTable了。

首先当sizeCtl < 0,则表示table正在调整大小,所以当前线程需要让步,也就是让在调整大小的线程先去执行;

然后会CAS的把sc调整为-1,也就是通知别的线程我要去初始化啦;

CAS成功后判断tab是否有值,还是空的话便初始化为默认容量。

posted @ 2020-07-12 23:25  被猪附身的人  阅读(156)  评论(0编辑  收藏  举报