Java-第十六部分-JUC-ConcurrentHashMap-jdk1.8

175 阅读6分钟

JUC全文

ConcurrentHashMap-jdk1.8

  • 底层也是调用unsafe的方法,进行CAS操作

初始化

  • 空构造采用懒加载的形式,只有添加第一个元素的时候才真正初始化
  • 有参构造,推荐使用,防止使用过程中扩容,影响性能
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               //获取大于传入的数的最近2的整数次幂
               //传入为initialCapacity * 0.5 initialCapacity + 1
               //则获取出来的是 > initialCapacity 的2的整数次幂 传入32 获取64 
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
  • sizeCtl,记录多线程场景下,初始化和扩容的阶段
  1. 为0,数组未初始化,默认的初始容量为16
  2. 为正数,如果数组未初始化,记录数组的初始容量,如果数组初始化,记录数组扩容的阈值(容量*0.75)
  3. 为-1,数组正在进行初始化
  4. <0,且不是-1,数组正在扩容,sizeCtl = Integer.MIN_VALUE + 2 + n表示有n个线程正在共同完成数组的扩容
  • put中真正初始化 image.png
  • initTable,CAS自旋操作进行,避免真正上锁
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0) //小于0,正在初始化/扩容,有其他线程正在操作
            Thread.yield(); // 释放掉cpu,不争抢,由执行状态,变为就绪
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //将sizeCtl设置为-1,设置失败后重新循环,让出CPU
            try {
                //再次判断,double-check,避免重复初始化
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //默认16
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); //设置阈值 sc = size - size / 4 = size * 0.75
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

put

  • 得出的哈希值一定是个正数,方便后面添加元素判断该节点的类型
int hash = spread(key.hashCode());
  • 一个死循环 image.png
  • 如果tab中当前角标没有值,在当前角标创建一个
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null,
                 new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}
  • 判断哈希值是否为MOVED -1,代表此时正在扩容,帮助扩容

会在角标处放一个forward节点,其哈希值为-1

else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);
  • 开始添加操作,锁对象为当前角标的对象,不影响其他的角标的操作
  1. if (tabAt(tab, i) == f) { double-check,防止添加前,其他线程进行了扩容,使得位置改变
  2. 哈希值大于0,为一个普通的链表结构,为一个单向链表
  3. 尾插法 image.png
  • 树结构的插入 image.png
  • 插入操作完毕,binCount记录节点数
if (binCount != 0) {
    //节点数>=8 进行树化
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null) //如果是替换了,直接返回旧值
        return oldVal;
    break;
}
  • 树化中,如果长度小于64,会用扩容解决 image.png

维护集合长度

  • put操作之后需要,维护集合长度,并检查是否需要扩容
addCount(1L, binCount);
  • addCount
  1. 尝试对baseCount++,成功即完成
  2. 尝试对ConterCell数组中的某个角标进行++,直到成功,其角标通过线程哈希得到
  3. 尝试对ConterCell数组++的过程中,如果失败,会对线程重哈希,换个角标进行
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //维护集合长度
    if ((as = counterCells) != null || //不为空的时候必然给CounterCell++
        //对baseCount进行尝试++
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        //累加失败 对CounterCell[]数组中++
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 || //数组为空
            (a = as[ThreadLocalRandom.getProbe() & m]) == null || //当前角标为null
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { //尝试放置
            //实际添加动作
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1) //当前角标只有一个节点,或者当remove时传入-1时,直接返回,不考虑扩容
            return;
        s = sumCount();
    }
    if (check >= 0) { //扩容,添加的时候才会满足这个条件
        Node<K,V>[] tab, nt; int n, sc;
        //如果大于阈值,且不为null,不越界
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //获取标记,1在第16位的位置,后面有15个0
            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)) //此时sc+1
                    //协助扩容
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         //rs左移16位,后面31个0,相当于1<<31,为一个负数
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //当前线程开始扩容
                transfer(tab, null);
            s = sumCount();
        }
    }
}
  • fullAddCount具体工作
  1. 拿到数组的随机值,计算CounterCell数组的角标
  2. 如果不存在CounterCell数组,尝试为其初始化,初始容量为2,需要尝试去对cellsBusy设置值,并且一次在初始化时只能有一个线程对cellsBusy设置
  3. 尝试为其初始化失败,则再次尝试对baseCount++,设置成功后跳出
  4. 初始化成功后,所有线程都将对CounterCell数组设置,当前线程的角标没有CounterCell对象,将x封装成CounterCell对象的value值中,设置成功后跳出
  5. 如果有,尝试给该角标对象加x,成功则跳出
  6. 失败,对CounteCell数组进行扩容,尽量避免扩容,当数组长度大于cpu核数或仅仅是因为没抢到cellsBusy
  7. 扩容后,对线程重哈希,重新找角标
  • resizeStamp,标记,标识开始扩容的状态,如果后续回到这个标识,则代表扩容完成
static final int resizeStamp(int n) {
    //1左移15位
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

集合长度累积

  • sumCount,累计baseCountCounterCell数组
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

扩容

  • 多线程协助扩容的时间点
  1. 尝试添加元素时,发现当前角标的元素为fwd
  2. 添加完之后,addCount维护集合长度时,发现sc标志位<0
  • transfer扩容,并迁移数据
  • 旧数组划分为几块,计算任务单元,最小是16个
//cpu<=1 交给一个线程
//cpu>1 右移三位 /8
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
    //如果小于16,那么就为16
    stride = MIN_TRANSFER_STRIDE; // subdivide range
  • 新建数组
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 = n; //旧数组的最大下标
}
  • 每一个迁移的节点,赋值一个ForwardingNode
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  • 从后往前开始迁移,分配任务
  1. bound任务的边界,通过nextBound获得
  2. i/nextIndex本次任务开始的下标
while (advance) {
    int nextIndex, nextBound;
    if (--i >= bound || finishing) 
        //i往前移动,还没有达到下一个次任务的边界,跳出while,进行当前任务的下一次迁移
        advance = false;
    else if ((nextIndex = transferIndex) <= 0) { //已经没有任务可以接了,那么直接为-1
        i = -1;
        advance = false;
    }
    //任务分配
    else if (U.compareAndSwapInt
             (this, TRANSFERINDEX, nextIndex,
              //更新边界
              nextBound = (nextIndex > stride ?
                           nextIndex - stride : 0))) {
        bound = nextBound; //记录下一次边界
        i = nextIndex - 1; //本次任务的开始下标
        advance = false;
    }
}
  • 迁移是否完成的逻辑,包含全部完成和单个线程完成,单个线程完成时可以进入
//i为当前任务的下标 n为旧数组的长度 nextn为新数组的长度
if (i < 0 || i >= n || i + n >= nextn) {
    int sc;
    if (finishing) { //全部完成
        nextTable = null;
        table = nextTab;
        //更新阈值 新size*0.75
        sizeCtl = (n << 1) - (n >>> 1);
        return;
    }
    //将sc设置为-1,当有线程协助扩容时,会令sc+1,当前线程完成后,sc-1
    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        //sc为 (rs << RESIZE_STAMP_SHIFT) + 2) rs在第31位+2,不相等时,扩容还未完成
        //当前线程先return,下次在此进入
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
            return;
        finishing = advance = true;
        i = n; // recheck before commit
    }
}
  • 当前位置为null,直接放fwd;当前位置已经是fwd不迁移,继续下一个任务
else if ((f = tabAt(tab, i)) == null)
    advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
    advance = true; // already processed
  • 真正迁移,为当前位置上锁,跟添加是一个锁 image.png
  • 单向链表的迁移过程与1.7类似
  1. 先找链表中末尾,连续的,可以放在同一个位置的链表直接移动过去
  2. 处理其他节点,通过hash & 旧长度快速得到放在原下标还是新下标
  3. 复制一份,头插法加入
  4. 迁移过的原数组的角标放上fwd
  • 树的迁移过程与hashMap类似,借助双向链表的结构迁移
  1. 将链表中的高低位分开
  2. 判断是否需要转换回单向链表
  3. 如果其中拆分了,重新树化,用的还是原节点

结构

ForwardingNode

  • 继承了Node,传入-1 MOVED的哈希值
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;
    }

TreeBin

  • 红黑树节点哈希为-2 image.png
  • 内部包含一个TreeNode实际操作,通过TreeNode进行操作 image.png

remove

public V remove(Object key) {
    return replaceNode(key, null, null);
}
  • 步骤
  1. 计算当前角标,如果当前角标为null,直接返回
  2. 当前角标位置为fwd,协助迁移
  3. 上锁,锁对象为当前角标对象,遍历当前角标的单向链表/红黑树
  • 找到有一个有效的节点 && 旧值不为null && 传入的value为null,维护集合长度

内部调用replaceNode(key, null, value),第二个参数为旧值,传入null,代表要删除 image.png

get

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) { //当前角标的元素
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0) //当前角标为红黑树,find查找,二叉搜索树的查找,由hash构建
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) { //遍历单向链表
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

size

  • 调用sumCount,累计baseCountCounterCell数组,这两个会在addCount中维护
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

小结

  • 结构tab[]+单向链表/红黑树(双向链表)
  • key完全相等的条件
  1. 哈希值相等
  2. 指向同一块内存区域/值相等,或者,equals方法使其相等
  • 保证线程安全
  1. 针对某一个角标上锁,put/remove/transfer
  2. 在发现一个节点为fwd时,会协助其他线程扩容
  3. 扩容动作时,将整个扩容区间分为最小为16的任务单元,由每个线程分别领取,线程完成当前任务后,会跳出
  4. CAS操作
  • 集合长度
  1. 添加元素完成后,会实时维护继承长度,通过baseCount+countercells的形式,分别保存节点数量,通过sum计算
  2. 获取结合长度直接通过sumCount()即可获得