• 并发编程学习笔记(二十八、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是否有值,还是空的话便初始化为默认容量。

  • 相关阅读:
    电话聊天狂人
    PAT 1080. MOOC期终成绩
    PAT 1079. 延迟的回文数
    [转载]matlab 中序列求极值的方法
    [转载][转]Matlab-寻找峰值函数
    [转载]Kirchhoff Migration Impulse Response
    Envelope Extraction Using the Analytic Signal
    [转载]地震动的挑选
    C笔记
    Git常用有用命令
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/13289187.html
Copyright © 2020-2023  润新知