ConcurrentHashMap源码阅读浅析


ConcurrentHashMap主体流程与HashMap有很多的相似处,可以先阅读HashMap源码再查看会比较容易理解

全局变量和类简介

//默认的初始化容量
private static final int DEFAULT_CAPACITY = 16;
//默认负载因子
private static final float LOAD_FACTOR = 0.75f;
//链表转红黑树的最小长度,前提是数组容量大于64 ,如果不大于64,则只是触发扩容
static final int TREEIFY_THRESHOLD = 8;
//链表转红黑树的数组必须的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
//红黑树转链表的最大节点数量
static final int UNTREEIFY_THRESHOLD = 6;
//扩容时候,每个线程最少迁移的桶数量
private static final int MIN_TRANSFER_STRIDE = 16;

//ForwardingNode对象节点的哈希值
static final int MOVED     = -1; // hash for forwarding nodes
//树跟节点封装成TreeBin对象的固定哈希值
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

//可用CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();

//链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

//红黑树根节点被封装成 TreeBin ,哈希值默认-2
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;

//红黑树节点
static final class TreeNode<K,V> extends Node<K,V>

//占位节点,哈希值默认-1 ,扩容时候重定向读到新数组,不用阻塞读
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;
    }

//当前ConcurrentHashMap存储使用的数组
transient volatile Node<K,V>[] table;
//扩容时候临时使用的数组
private transient volatile Node<K,V>[] nextTable;
//记录存储数量size的值,不精确
private transient volatile long baseCount;

//记录扩容时候迁移任务分配的到桶的位置,从最高位开始
private transient volatile int transferIndex;
//记录更新baseCount值失败后,存储需要增加的计数
private transient volatile CounterCell[] counterCells;

//关键属性
//负数表示控制初始化和扩容的标记,
// -1 的时候正在初始化数组  
// -(1+活跃的扩容线程数)表示在扩容,并记录线程数,线程扩容结束会减少记录
//当table=null 表示需要创建数组的容量大小,如果此时为0 则使用默认值容量16
//初始化后,则表示控制下一次扩容的大小阈值
private transient volatile int sizeCtl;

Hash算法

//获取hash值与上 0x7fffffff(31位都是1),保证hash值不小于0
static final int spread(int h) {
  return (h ^ (h >>> 16)) & HASH_BITS;
}

Put方法实现

//在put过程中,如果哈希冲突,需要存放到树或者链表上,则对树的根节点或者链表头节点使用 synchronized加锁
//在链表转红黑树过程中,会对链表头节点使用 synchronized加锁
final V putVal(K key, V value, boolean onlyIfAbsent) {
  //获取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();
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
      //数组位置上没有元素,CSA直接存放
      if (casTabAt(tab, i, null,
              new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
    }
  //hash值等于-1 ,表示改位置有线程正在扩容,则当前线程协助扩容,扩容结束后再走存放逻辑
    else if ((fh = f.hash) == MOVED)
      tab = helpTransfer(tab, f);
    else { //该位置没有扩容
      V oldVal = null;
      synchronized (f) { //对第一个节点加锁
        if (tabAt(tab, i) == f) {
          if (fh >= 0) { //hash算法决定了hash值肯定大于等于 0 ,链表的hash值不小于0
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
              K ek;
              //判断相同的key,则覆盖value
              if (e.hash == hash &&
                      ((ek = e.key) == key ||
                              (ek != null && key.equals(ek)))) {
                oldVal = e.val;
                if (!onlyIfAbsent)
                  e.val = value;
                break;
              }
              //没有相同的key,构建node放到链表尾部
              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;
            }
          }
        }
      }
      //链表长度大于8,,数组长度大于64触发转红黑树
      if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
          treeifyBin(tab, i);
        if (oldVal != null)
          return oldVal;
        break;
      }
    }
  }
  addCount(1L, binCount);
  return null;
}

数组初始化方法

private final Node<K,V>[] initTable() {
  Node<K,V>[] tab; int sc;

  while ((tab = table) == null || tab.length == 0) {
    //判读如果小于零,说明有线程去初始化了,当前线程进行礼让自旋
    if ((sc = sizeCtl) < 0)
      Thread.yield(); // lost initialization race; just spin
    //CAS判断 sizeCtl内存地址处的值与 sc的值一样 ,则重置为-1 ,进行初始化,再进来的线程判断则走自旋
    else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
      try {
        if ((tab = table) == null || tab.length == 0) {
          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
          @SuppressWarnings("unchecked")
          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
          table = tab = nt;
          //获取下一次扩容的阈值
          sc = n - (n >>> 2); //n >>> 2就是除以4 ,计算式相当于n*0.75,位运算更高效
        }
      } finally {
        //设置一次扩容的阈值
        sizeCtl = sc;
      }
      break;
    }
  }
  return tab;
}

Size计数的方法

BASECOUNT 存储数组存放的元素个数,但是不是准确值,因为CAS更新这个值的时候会失败,失败后会创建 CounterCell记录需要增加的值放到 CounterCell[] counterCells数组中。 size计算就是 BASECOUNT 加上 数组中的值之和

private final void addCount(long x, int check) {
  CounterCell[] as; long b, s;
  //CAS添加计数的BASECOUNT值
  if ((as = counterCells) != null ||
          !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
    CounterCell a; long v; int m;
    boolean uncontended = true;
    //CAS添加计数的BASECOUNT值失败,则CAS添加CounterCell中记录的值
    if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
                    U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
      //死循环直到则CAS添加CounterCell中记录的值成功
      fullAddCount(x, uncontended);
      return;
    }
    if (check <= 1)
      return;
    s = sumCount();
  }
  //校验是否需要扩容
  if (check >= 0) {
    Node<K,V>[] tab, nt; int n, sc;
    while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
            (n = tab.length) < MAXIMUM_CAPACITY) {
      int rs = resizeStamp(n);
      if (sc < 0) {
        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                transferIndex <= 0)
          break;
        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
          transfer(tab, nt);
      }
      else if (U.compareAndSwapInt(this, SIZECTL, sc,
              (rs << RESIZE_STAMP_SHIFT) + 2))
        transfer(tab, null);
      s = sumCount();
    }
  }
}

协助扩容方法

当put的时候,发现正在扩容,该线程会加入扩容大军进行协助扩容

//协助扩容方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
  Node<K,V>[] nextTab; int sc;
  //再次判断确认数组不为空,且槽位上节点被 ForwardingNode 占据表名正在扩容
  if (tab != null && (f instanceof ForwardingNode) &&
          (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
    int rs = resizeStamp(tab.length);
    //判断扩容没有结束,死循环申请扩容任务
    while (nextTab == nextTable && table == tab &&
            (sc = sizeCtl) < 0) {
      if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
              sc == rs + MAX_RESIZERS || transferIndex <= 0)
        break;
      //扩容线程数加 1 ,进行扩容
      if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
        transfer(tab, nextTab);
        break;
      }
    }
    return nextTab;
  }
  return table;
}

核心: 扩容方法

//扩容方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  int n = tab.length, stride;
  //计算每个扩容线程至少迁移的槽位数量
  if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
    stride = MIN_TRANSFER_STRIDE; // subdivide range
  //初始化新数组用于迁移
  if (nextTab == null) {            // initiating
    try {
      @SuppressWarnings("unchecked")
      Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
      nextTab = nt;
    } catch (Throwable ex) {      // try to cope with OOME
      sizeCtl = Integer.MAX_VALUE;
      return;
    }
    nextTable = nextTab;
    transferIndex = n; //迁移从原数组最高位置开始
  }
  int nextn = nextTab.length;
  //创建重定向节点:1.占位标桶位置记已经迁移完成
  //2.转发作用,当get的时候遇到这个节点,则转发到新数组去查询,而不用阻塞
  ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  boolean advance = true; //继续推进桶迁移的标记
  boolean finishing = false; // 迁移完成的标记
  for (int i = 0, bound = 0;;) {
    Node<K,V> f; int fh;
    while (advance) {
      int nextIndex, nextBound;
      if (--i >= bound || finishing)
        advance = false;
      else if ((nextIndex = transferIndex) <= 0) {
        //transferIndex <= 0 说明数组的hash桶已被线程分配完毕,没有了待分配的hash桶,将 i 设置为 -1 ,
        // 后面的代码根据这个数值退出当前线的扩容操作
        i = -1;
        advance = false;
      }
      //只有首次进入for循环才会进入这个判断里面去,设置 bound 和 i 的值,也就是领取到的迁移任务的数组区间
      else if (U.compareAndSwapInt
              (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                              nextIndex - stride : 0))) {
        //设置迁移任务的边界值
        bound = nextBound;
        i = nextIndex - 1;
        advance = false;
      }
    }
    if (i < 0 || i >= n || i + n >= nextn) {
      int sc;
      //扩容结束,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
      if (finishing) {
        nextTable = null;
        table = nextTab;
        sizeCtl = (n << 1) - (n >>> 1);
        return;
      }
      //每条线程扩容结束都会将扩容线程数量减一
      if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        //判断是否为最后一条扩容线程
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
          return;
        //除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
        finishing = advance = true;
        i = n; // recheck before commit
      }
    }
    //判断该桶位置为空,则使用占位节点进行占位
    else if ((f = tabAt(tab, i)) == null)
      advance = casTabAt(tab, i, null, fwd);
    //判断是占位节点,则处理下一个桶
    else if ((fh = f.hash) == MOVED)
      advance = true; // already processed
    else {
      //锁住节点,进行迁移
      synchronized (f) {
        if (tabAt(tab, i) == f) {
          Node<K,V> ln, hn;
          //迁移链表
          if (fh >= 0) {
            //拿到链表头节点的哈希值与数组长度的与值,判断段是高位链还是低位链节点
            int runBit = fh & n;
            Node<K,V> lastRun = f;
            for (Node<K,V> p = f.next; p != null; p = p.next) {
              int b = p.hash & n;
              //判断节点是与头节点属于同一个链表(低位链或者高位链)
              if (b != runBit) {
                runBit = b;
                lastRun = p;
              }
            }
            //判断将lastRun节点设置为高位链或者低位链头节点
            if (runBit == 0) {
              ln = lastRun;
              hn = null;
            }
            else {
              hn = lastRun;
              ln = null;
            }
            //循环链表只读lastRun节点,使用头插法复制拼接高位链表和低位链表
            //链表中lastRun节点以后的节点与lastRun节点是同一个链上的,不需要继续遍历了,这就是lastrun机制
            for (Node<K,V> p = f; p != lastRun; p = p.next) {
              int ph = p.hash; K pk = p.key; V pv = p.val;
              if ((ph & n) == 0)
                ln = new Node<K,V>(ph, pk, pv, ln);
              else
                hn = new Node<K,V>(ph, pk, pv, hn);
            }
            //将高低位链表分别存到新数组对应的桶中,将旧数组的桶元素用ForwardNdoe占位
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
          }
          //迁移红黑树,使用的是尾插法
          else if (f instanceof TreeBin) {
            TreeBin<K,V> t = (TreeBin<K,V>)f;
            TreeNode<K,V> lo = null, loTail = null;
            TreeNode<K,V> hi = null, hiTail = null;
            int lc = 0, hc = 0;
            //使用链表的形式遍历红黑树(红黑树维护了一条链表),分成高低位两部分,使用尾插法连接
            for (Node<K,V> e = t.first; e != null; e = e.next) {
              int h = e.hash;
              TreeNode<K,V> p = new TreeNode<K,V>
                      (h, e.key, e.val, null, null);
              if ((h & n) == 0) {
                if ((p.prev = loTail) == null)
                  lo = p;
                else
                  loTail.next = p;
                loTail = p;
                ++lc;
              }
              else {
                if ((p.prev = hiTail) == null)
                  hi = p;
                else
                  hiTail.next = p;
                hiTail = p;
                ++hc;
              }
            }
            //判断是否需要红黑树转成链表,不需要则封装成TreeBin
            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
            //设置到新数组对应位置,使用占位节点覆盖旧数组对应的桶元素
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
          }
        }
      }
    }
  }
}

面试题及答案

1. 说说Put 流程

在这里插入图片描述
数组初始化流程如下:
在这里插入图片描述

2. 谈谈扩容流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 扩容中,读能够访问到数据,是怎么实现的?

  1. 当读取数据的桶没有进行迁移,按照原来的方式正常访问。
  2. 当读取到的桶正在迁移,因为迁移桶中的数据是直接赋值出来形成 ln hn 链的,不会影响原来的数据,可以直接访问而不会阻塞 get 操作。
  3. 当读取到的桶已经迁移完成,此时是 ForwardingNode 节点,里面持有了新数组引用,此次会重定向 get 操作到新数组中进行查找操作

4.库容过程中,写操作如何处理?

  1. 当写的桶位置还没有进行迁移,正常写入
  2. 当写的桶位置拿到元素哈希值为-1,表明正在进行扩容,当前线程加入扩容中协助扩容,扩容结束后再进行写入
  3. 当写的桶位置正在进行迁移,此时桶第一个元素节点被上锁,会阻塞写操作

5.指定桶位置形成了红黑树,并且红黑树正在进行自平衡,此时读操作怎么处理?

读线程会先判断 lockState 锁状态位是否被其它线程持有写锁或者等待写锁,
如果其它线程持有写锁,则以链表的形式遍历红黑树来进行读取操作;
如果其它线程持有了读锁,则 lockState 状态位加 4, 读锁可重入,使用红黑树的方式进行遍历查询数据。

6.在 1.8 版本中,统计存储元素个数是怎么实现的,为什么不适用AtomicLong 来进行统计?

在 put 操作结束后,会调用 addCount,更新计数,使用 CAS 更新 baseCount 统计个数,当 CAS 操作失败后;使用 CAS 更新 CounterCell[]数组记录要增加的数量,如果继续失败,则进行死循环操作进行添加,直到成功。所以 baseCount 加上数组中存储值的总和就是当前容器在统计时候的个数。
不使用 AtomicLong 进行计数是因为,在高并发的场景下,原子操作可能会导致失败,这个时候就没法继续处理了。

7.简单说一下 LastRun 机制

在这里插入图片描述

8.如果 lastRun 节点正好在一条全部都为高位或者全部都为低位的链表上,会不会形成死循环?

扩容的时候是使用赋值原来的节点数据进行扩容的,而不是操作原有的节点,不会存
在形成死循环的条件。因为是复制的迁移方式,所以在迁移过程中,会导致 Map 占用内存空间增翻一倍。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值