几个参数 没法拿下ConcurrentHashMap

156 阅读11分钟

dddd~ 这周我们来拿下ConcurrentHashMap(JDK1.8)

前序

ConcurrentHashMap可以理解为是并发安全的HashMap,在JDK1.7及之前,HashMap在resize()方法中使用头插法进行扩容,这样的操作也给HashMap带来了并发安全的问题,至于为什么会,后续会在出一篇文章解释。在JDK1.8以后,改成了尾插法进行扩容,这样就避免了resize()并发安全问题,但对于HashMap而言,在高并发的情况下还是会存在如:死循环(多个线程同时修改同一个数据,导致数据结构的不一致性,进而在尝试访问或修改数据时陷入无限循环)等,所以在高并发的情况下,强烈推荐大家使用ConcurrentHashMap.

本篇文章,我们涉及的部分有主要参数、整体结构、构造方法、put()方法、initTable()方法

主要参数

/**数组的最大长度*/
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**默认的初始表容量,一定是2的n次幂*/
private static final int DEFAULT_CAPACITY = 16;

/** 最大可能的(非2的幂)数组大小*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**该表的默认并发级别。未使用但为了与这个类以前的版本兼容而定义的。*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**负载因子 用于计算扩容的阈值*/
private static final float LOAD_FACTOR = 0.75f;

/** 链表转为红黑树存储的阈值*/
static final int TREEIFY_THRESHOLD = 8;

/** 解除红黑树存储的阈值*/
static final int UNTREEIFY_THRESHOLD = 6;

/** 最小转为红黑树的数组长度*/
static final int MIN_TREEIFY_CAPACITY = 64;

/**每个传输步骤的最小重新绑定数。范围被细分以允许多个调整大小线程。此值用作下限,以避免调整大小器遇到过多的内存争用。该值应为“至少” DEFAULT_CAPACITY。*/
private static final int MIN_TRANSFER_STRIDE = 16;

/**在sizeCtl中用于生成戳记的位数。对于32位数组,必须至少为6。*/
private static int RESIZE_STAMP_BITS = 16;

/** 可以帮助调整大小的最大线程数。*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/** 在sizeCtl中记录尺寸戳的位移。*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

/**表初始化和调整大小控件。当为负值时,表正在初始化或调整大小:-1表示初始化,否则-(1 +活动调整大小线程的数量)。否则,当table为null时,将保留创建时使用的初始表大小,或者保留默认值为0。初始化后,保存要调整表大小的下一个元素计数值。*/
private transient volatile int sizeCtl;

整体结构

image.png 由图可见数组中存放的是一个一个的节点,volatile 修饰的val保证了val值的可见性(也就是只要这个值发生变化了,都会被刷到主内存中,别的线程的内存对应该数据都会被刷新 ps:这一块可以学习一下JMM)

与HashMap的第一个不同处:对应的节点数组,val值 都是由volatile来进行修饰

构造方法

    /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }
    
    //指定容量初始化
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
    //容量 = 初始化容量*3/2+1的最近的2次幂的值
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
     //这里的tableSizeFor跟HashMap的是一致的
    //阈值 = 容量
        this.sizeCtl = cap;
    }
    /**
    指定容量和负载因子初始化
    */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }
  
    //指定容量和负载因子初始化和并发级别
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //最小的容量都要满足并发的等级
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

总体来看,跟HashMap的出入不大,都是初始化了数组的阈值。看到这还是会比较模糊的吧~所以我们接下来从put方法入手,解析在构造方法中用到的sizeCtl都在哪用到了

put()

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

同hashMap,都是通过调用名称为putVal()这个方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //这里,最让大家知道它与HashMap有不同的地方!ps:文章末我们做解释
    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();
          //如果当前节点数组的位置为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //尝试通过CAS将该值设置到该索引上
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                   //成功则返回
                break;                  
        }
        //如果判断到当前节点处于MOVED状态(也就是在扩容的状态)
        else if ((fh = f.hash) == MOVED)
            //那么就帮助扩容并且返回新数组
            tab = helpTransfer(tab, f);
        else {
        //否则,就是到了遍历链表的时候了
            V oldVal = null;
            //这是jdk1.7和jdk1.8不同的地方之一(1.8是锁当前的数组节点)当前只有一个线程能够操作
            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;
                               //onlyIfAbsent 默认为false
                                if (!onlyIfAbsent)
                                //替换值
                                    e.val = value;
                                break;
                            }
                            //如果遍历到链表尾,就进行一个next指针的指向
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //如果当前节点是红黑树,那么就进行红黑树的赋值
                    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;
                        }
                    }
                }
            }
            //判断当前链表长度是否达到树形化的阈值
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

initTable()

在判断当前数组为空的情况,那么就进行数组的初始化

private final Node<K,V>[] initTable() {
            Node<K,V>[] tab; int sc;
             //当数组为空的时候(第一次检查)
            while ((tab = table) == null || tab.length == 0) {
                //如果当前的siezCtl<0的时候 (代表有线程在执行扩容)
                if ((sc = sizeCtl) < 0)
                    //释放当前CPU的资源
                    Thread.yield(); 
                    //如果sizeCtl>=0,那么就通过cas将当前的sizeCtl设置为-1,代表当前线程获取初始化的时间片,进行初始化
                else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        //第二次检查,避免并发下,防止其他线程在当前线程等待时已经完成初始化
                        if ((tab = table) == null || tab.length == 0) {
                            //初始化长度为 sizeCtl/默认长度16
                            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*3/4
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                    break;
                }
            }
            return tab;
        }

初始化方法在于两个实现关键点:

  1. sizeCtl值的判断
  2. 双重检查,避免多次初始化

helpTransfer()

在put方法中,我们看到了有一个神奇的方法,在当前线程在执行put方法的时候,如果当前索引的数组节点被标记为MOVE那么就会执行该方法,那这个方法究竟是做什么的呢?

  1. 从方法名字看,帮助移植,那我们就能初步的理解为帮忙扩容的?让我们打开源码,一探究竟

forwardingNode<K,V>

首先,我们了解一下这个节点是做什么的


     // A node inserted at head of bins during transfer operations.
     // 在传输操作期间插入到箱子头部的节点
    static final class ForwardingNode<K,V> extends Node<K,V> {
        //nextTable指向的是新的数组
        final Node<K,V>[] nextTable;
        //当原数组中的桶数据进行迁移完毕,那么该桶节点就会被标记为ForwardingNode
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
        ......
    }

从方法中,我们可以看出这个方法就是作为一个判断的辅助节点,用于告知线程当前桶中的所有节点是否都已经扩容转移完成了,也就代表原数组在执行扩容,所以才会有我们下面方法中的判断是否索引节点是否是ForwardingNode。

当前线程表示,我也想插一腿帮你扩容一下

helperTransfer()

/**
*@param tab 当前节点数组
*@param f 当前索引节点
*/
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;
                //通过cas增加sizeCtl,然后进行扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    //进行实际的扩容
                    transfer(tab, nextTab);
                    break;
                }
            }
            //返回新的数组
            return nextTab;
        }
        //否则返回原数组
        return table;
    }

    /**
     * Returns the stamp bits for resizing a table of size n.
     * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
     */
    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

插一腿进来以后就看能不能拿到对应的工具,能拿到对应的工具(cas修改sizeCtl成功)后,你就可以去执行transfer方法了,那就可以开始扩容啦

transfer()

铺垫了那么久,终于 transfer()出来辽。 这个方法,我们分为以下部分进行阐述:

  1. 计算线程的步长,就是每个线程执行的区间长
 /**
     * Moves and/or copies the nodes in each bin to new table. See
     * above for explanation.
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //计算每个线程的扩容步长,也就是每个线程负责的区间大小(跟服务器的配置有关,根据CPU数量进行分配)
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; 
        //如果新数组为空,进行新数组的初始化,扩容为原来的两倍
        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;
        }
        ..........
  }

根据cpu进行每个线程负责区间长的进行计算,如果没有初始化新数组,扩容为原来的两倍

  1. 进行对每个线程数组索引区间的分配
 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
     计算步长和初始化完新数组
      ....
        //新数组长度
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        
        // 用于判断当前线程负责的区域是否完成 是否到下一个桶
        boolean advance = true;
        // 用于判断当前扩容是否已经结束
        boolean finishing = false; 
        // i 是 索引下标 bound代表当前线程处理的上界限
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //当advance为true
            while (advance) {
                int nextIndex, nextBound;
                //当前线程完成设定区间||分配完成
                if (--i >= bound || finishing)
                    advance = false;
                //当transferIndex <=0也就是所有线程分配完成 transferIndex是一个全局变量
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //通过CAS修改nextBound值
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    //如果能通过CAS设定新的transferindex 并且更新 bound 和i值
                    //例如步长为4 数组长度为16 那么transferindex在 CAS成功的情况下 分别是 
                    //16 12 8 4 0
                    bound = nextBound;//12 8 4 0
                    i = nextIndex - 1;//15 11 7 3 -1 
                    advance = false; //当前线程分配完成
                    //区间就被分配为[i,bound]
                }
            }
            ........
      }
  1. 每个线程对自己负责的区间进行扩容
        for (int i = 0, bound = 0;;) {
           ......
            //实际迁移过程
            //如果i<0 或者i>=数组长度||i+n>=新数组长度
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //如果resize()(扩容)完成
                if (finishing) {
                    //nextTable置为null 释放内存,帮助GC
                    nextTable = null;
                    //新旧数组交替
                    table = nextTab;
                    //sizeCtl阈值等于 n*2-n/2;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //通过cas将sc减一
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //如果当前sc-2不等于当前扩容状态
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //否则表示扩容完成
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
                //如果当前旧数组节点为空
            else if ((f = tabAt(tab, i)) == null)
                //通过cas将旧数组节点置为fwd节点,代表该节点已经被处理过
                advance = casTabAt(tab, i, null, fwd);
                //如果正在执行扩容
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
                //同步处理各个桶数据
            else {
                synchronized (f) {
                    //tabAt(),通过volatile关键字获取最新的value值
                    if (tabAt(tab, i) == f) {
                        //这里同HashMap的扩容,高低位链表
                        //处理链表数据,
                        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;
                                }
                            }
                           
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            //循环更新ln 和hn 不断将next指针指向新的后一个节点
                            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);
                            }
                            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;
                                }
                            }
                            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;
                        }
                    }
                }
            }
        }
    }

欢迎大家指出不正确的地方和不明白的地方,我们一起探讨