25岁的JAVA,还有多少时间?

标题:

一、历史版本的实现演变
二、重要成员属性的介绍
三、put 方法实现并发添加
四、remove 方法实现并发删除
五、其他的一些常用方法的基本介绍
—————————————————————————
HashMap 是我们日常最常见的一种容器,它以键值对的形式完成对数据的存储,但众所周知,它在高并发的情境下是不安全的。尤其是在 jdk 1.8 之前,rehash 的过程中采用头插法转移结点,高并发下,多个线程同时操作一条链表将直接导致闭链,死循环并占满 CPU。

当然,jdk 1.8 以来,对 HashMap 的内部进行了很大的改进,采用数组+链表+红黑树来进行数据的存储。rehash 的过程也进行了改动,基于复制的算法思想,不直接操作原链,而是定义了两条链表分别完成对原链的结点分离操作,即使是多线程的情况下也是安全的。

当然,它毕竟不是并发容器,除非大改,否则依然是不能应对高并发场景的,或者说即使没有因多线程访问而造成什么问题,但是效率必然是受到影响的。例如在多线程同时添加元素的时候可能会丢失数据,迭代 map 的时候发生 fail-fast 等。

一、历史版本的实现演变

           jdk 1.7 采用分段锁技术,整个 Hash 表被分成多个段,每个段中会对应一个 Segment 段锁,段与段之间可以并发访问,但是多线程想要操作同一个段是需要获取锁的。所有的 put,get,remove 等方法都是根据键的 hash 值对应到相应的段中,然后尝试获取锁进行访问。

              jdk 1.8 取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作,在某些方面提升了性能。并且追随 1.8 版本的 HashMap 底层实现,使用数组+链表+红黑树进行数据存储。本篇主要介绍 1.8 版本的 ConcurrentHashMap 的具体实现。

二、重要成员属性的介绍

    transient volatile Node<K,V>[] table;
    和 HashMap 中的语义一样,代表整个哈希表。
    
    private transient volatile Node<K,V>[] nextTable;
    这是一个连接表,用于哈希表扩容,扩容完成,扩容完成后会被重置为 null。
   
    private transient volatile long baseCount;
    该属性保存着整个哈希表中存储的所有的结点的个数总和,有点类似于 HashMap 的 size 属性。
  
    private transient volatile int sizeCtl;
    这是一个重要的属性,无论是初始化哈希表,还是扩容 rehash 的过程,都是需要依赖这个关键属性的。该属性有以下几种取值:

0:默认值
-1:代表哈希表正在进行初始化
大于0:相当于 HashMap 中的 threshold,表示阈值
小于-1:代表有多个线程正在进行扩容

该属性的使用还是有点复杂的,在我们分析扩容源码的时候再给予更加详尽的描述,此处了解其可取的几个值都分别代表着什么样的含义即可。

构造函数的实现也和我们之前介绍的 HashMap 的实现类似,此处不再赘述,贴出源码供大家比较。

    public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
    throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
           MAXIMUM_CAPACITY :
           tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;

}

                其他常用的方法我们将在文末进行简单介绍,下面我们主要来分析下 ConcurrentHashMap 的一个核心方法 put,我们也会一并解决掉该方法中涉及到的扩容、辅助扩容,初始化哈希表等方法。

三、put 方法实现并发添加

对于 HashMap 来说,多线程并发添加元素会导致数据丢失等并发问题,那么ConcurrentHashMap 又是如何做到并发添加的呢?

        public V put(K key, V value) {
             return putVal(key, value, false);
        } 

putVal 的方法比较多,我们分两个部分进行分析。

        //第一部分
   final V putVal(K key, V value, boolean onlyIfAbsent) {
//对传入的参数进行合法性判断
if (key == null || value == null) throw new NullPointerException();
//计算键所对应的 hash 值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
    Node<K,V> f; int n, i, fh;
    //如果哈希表还未初始化,那么初始化它
    if (tab == null || (n = tab.length) == 0)
        tab = initTable();
    //根据键的 hash 值找到哈希数组相应的索引位置
    //如果为空,那么以CAS无锁式向该位置添加一个节点
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null,
                     new Node<K,V>(hash, key, value, null)))
            break;
    } 

这里需要详细说明的只有 initTable 方法,这是一个初始化哈希表的操作,它同时只允许一个线程进行初始化操作。

         private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//如果表为空才进行初始化操作
while ((tab = table) == null || tab.length == 0) {
    //sizeCtl 小于零说明已经有线程正在进行初始化操作
    //当前线程应该放弃 CPU 的使用
    if ((sc = sizeCtl) < 0)
        Thread.yield(); // lost initialization race; just spin
    //否则说明还未有线程对表进行初始化,那么本线程就来做这个工作
    else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
        //保险起见,再次判断下表是否为空
        try {
            if ((tab = table) == null || tab.length == 0) {
                //sc 大于零说明容量已经初始化了,否则使用默认容量
                int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                @SuppressWarnings("unchecked")
                //根据容量构建数组
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                table = tab = nt;
                //计算阈值,等效于 n*0.75
                sc = n - (n >>> 2);
            }
        } finally {
            //设置阈值
            sizeCtl = sc;
        }
        break;
    }
}
return tab;

}

关于 initTable 方法的每一步实现都已经给出注释,该方法的核心思想就是,只允许一个线程对表进行初始化,如果不巧有其他线程进来了,那么会让其他线程交出 CPU 等待下次系统调度。这样,保证了表同时只会被一个线程初始化。

接着,我们回到 putVal 方法,这样的话,我们第一部分的 putVal 源码就分析结束了,下面我们看后一部分的源码:

          //检测到桶结点是 ForwardingNode 类型,协助扩容
   else if ((fh = f.hash) == MOVED)
 tab = helpTransfer(tab, f);
   //桶结点是普通的结点,锁住该桶头结点并试图在该链表的尾部添加一个节点
    else {
   V oldVal = null;
   synchronized (f) {
       if (tabAt(tab, i) == f) {
          //向普通的链表中添加元素,无需赘述
          if (fh >= 0) {
             binCount = 1;
             for (Node<K,V> e = f;; ++binCount) {
                 K ek;
                 if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {
                     oldVal = e.val;
                     if (!onlyIfAbsent)
                        e.val = value;
                        break;
                  }
                  Node<K,V> pred = e;
                  if ((e = e.next) == null) {
                     pred.next = new Node<K,V>(hash, key,value, null);
                     break;
                  }
             }
       }
       //向红黑树中添加元素,TreeBin 结点的hash值为TREEBIN(-2)
       else if (f instanceof TreeBin) {
           Node<K,V> p;
           binCount = 2;
             if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
               oldVal = p.val;
               if (!onlyIfAbsent)
                  p.val = value;
            }
       }
   }

}

       //binCount != 0 说明向链表或者红黑树中添加或修改一个节点成功
  //binCount  == 0 说明 put 操作将一个新节点添加成为某个桶的首节点
  if (binCount != 0) {
     //链表深度超过 8 转换为红黑树
     if (binCount >= TREEIFY_THRESHOLD)
         treeifyBin(tab, i);
     //oldVal != null 说明此次操作是修改操作
     //直接返回旧值即可,无需做下面的扩容边界检查
     if (oldVal != null)
         return oldVal;
       break;
    }
}

}

      //CAS 式更新baseCount,并判断是否需要扩容
  addCount(1L, binCount);
    //程序走到这一步说明此次 put 操作是一个添加     操作,否则早就 return 返回了
   return null; 

这一部分的源码大体上已如注释所描述,至此整个 putVal 方法的大体逻辑实现相信你也已经清晰了,好好回味一下。下面我们对这部分中的某些方法的实现细节再做一些深入学习。
首先需要介绍一下,ForwardingNode 这个节点类型,

   static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        //注意这里
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
//省略其 find 方法

}

这个节点内部保存了一 nextTable 引用,它指向一张 hash 表。在扩容操作中,我们需要对每个桶中的结点进行分离和转移,如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。

ForwardingNode 继承自 Node 结点,并且它唯一的构造函数将构建一个键,值,next 都为 null 的结点,反正它就是个标识,无需那些属性。但是 hash 值却为 MOVED。

所以,我们在 putVal 方法中遍历整个 hash 表的桶结点,如果遇到 hash 值等于 MOVED,说明已经有线程正在扩容 rehash 操作,整体上还未完成,不过我们要插入的桶的位置已经完成了所有节点的迁移。

由于检测到当前哈希表正在扩容,于是让当前线程去协助扩容。

     final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
    (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
    //返回一个 16 位长度的扩容校验标识
    int rs = resizeStamp(tab.length);
    while (nextTab == nextTable && table == tab &&
           (sc = sizeCtl) < 0) {
        //sizeCtl 如果处于扩容状态的话
        //前 16 位是数据校验标识,后 16 位是当前正在扩容的线程总数
        //这里判断校验标识是否相等,如果校验符不等或者扩容操作已经完成了,直接退出循环,不用协助它们扩容了
        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
            sc == rs + MAX_RESIZERS || transferIndex <= 0)
            break;
        //否则调用 transfer 帮助它们进行扩容
        //sc + 1 标识增加了一个线程进行扩容
        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
            transfer(tab, nextTab);
            break;
        }
    }
    return nextTab;
}
return table;

}

下面我们看这个稍显复杂的 transfer 方法,我们依然分几个部分来细说。

         //第一部分
        private final void transfer(Node<K,V>[] tab,     
             Node<K,V>[] nextTab) {
             int n = tab.length, stride;
     
      //计算单个线程允许处理的最少table桶首节点个数,不能小于 16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;
    //刚开始扩容,初始化 nextTab
    if (nextTab == null) {
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        //transferIndex 指向最后一个桶,方便从后向前遍历
        transferIndex = n;
    }
    int nextn = nextTab.length;
    //定义 ForwardingNode 用于标记迁移完成的桶
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

这部分代码还是比较简单的,主要完成的是对单个线程能处理的最少桶结点个数的计算和一些属性的初始化操作。

猜你喜欢

转载自blog.csdn.net/weixin_42584783/article/details/108896264
今日推荐