JDK1.8中ConcurrentHashMap全源码解读(初始化机制、自动扩容机制、Put方法)

本章节基于源码进行逐行分析,请大家结合源码一点一点看;

构造方法

如果看过JDK1.7中ConcurrentHashMap源码的同学可以知道,JDK1.7的无参构造方法中进行了很多数据的运算进行初始化,而在JDK1.8中,无参方法就是无参方法,没有其他的操作;

那么我们直接点进put方法看:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;

我们可以很明显的看到,ConcurrentHashMap是不允许key或者value为空的

然后基于当前key计算了一个hash值,然后定义了一个bitCount;

然后往下走,进入了一个for循环:

  for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

在这个for循环中,首先拿到当前操作的数组对象,然后判断是否为null,如果为null则调用initTable方法进行初始化,那我们就来看看ConcurrentHashMap的初始化机制:

初始化机制

    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
            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);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

1.它使用了一个自旋的方式(保证在这个过程中数组没有被其他线程初始化

2.第一个判断sc(默认为0)是否小于0,这个值就是一个记录参数,如果小于0则调用Thread类的yield方法,该方法的意思是让当前这个线程暂时放弃当前CPU的资源,让它重新去竞争资源

3.默认情况下走下面的分支,通过CAS操作,让sc减一并且存入内存中,因此如果有多个线程进行CAS的话,只有一个线程能够减成功,其他线程重新进入循环的时候再去进行判断sc就会拿到内存中已经减一的值,那么就会放弃当前CPU的资源,保证了线程安全;

4.减成功之后再去进行一次数组为null的判断

5.然后去获取数组的容量:如果我们使用了有参构造器指定sc的话就使用sc,如果我们没有指定初始化容量那么就使用默认的初始化容量(默认为16)

6.创建Node数组,并赋予容量

7.再去记录一下sc与sizeCtl,这个时候sc就等于初始容量n减去初始容量n左移两位(四分之一)也就是0.75乘以n

8.相当于在初始化数组的时候我们就记录了后面扩容的阈值(sizeCtl)

至此初始化结束;

那么继续往下看,如果数组已经初始化过了,进入第二次循环,进入真正的put流程:

    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
            }

这里调用了两个方法,一个是tabAt一个是casTabAt方法,我们来看看这两个方法是什么:

 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

tabAt:基于Unsafe的方法在内存中去拿值

casTabAt:基于CAS的设置值的操作

1.因此这里首先基于当前索引值在数组中去获取链表的头节点,如果头节点为null的话,说明当前数组位置是为null的,那么则生成一个Node对象,基于CAS设置值,如果CAS操作成功则返回true则跳出循环;

如果当前链表头节点不为空,则进入下一个分支进行判断:

    else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

判断当前头节点的hash值是否等于默认值MOVED(-1),若等于的话说明有其他线程正在对当前数组进行扩容操作,则调用helpTransfer方法帮助扩容

如果该条件也不满足,说明可以正常执行当前链表中的put操作:

//(f = tabAt(tab, i = (n - 1) & hash)
synchronized (f) {
    xxx
}

1.首先对整个操作加锁,我们可以知道,这个f是当前数组位置的链表头节点,因此可以发现加锁的对象是这个头节点

if (tabAt(tab, i) == f) {
    if (fh >= 0) {  //fh=f.hash

2.再次判断当前位置头节点是没有发生改变的(避免加锁期间有其他线程操作了当前链表),然后再判断当前fh是否大于0(确保当前节点是链表的节点而非红黑树的节点

for (Node<K,V> e = f;; ++binCount) {

3.拿到头节点并遍历当前链表

if (e.hash == hash &&
    ((ek = e.key) == key ||
     (ek != null && key.equals(ek)))) {
    oldVal = e.val;
 if (!onlyIfAbsent)
 e.val = value;
        break;
}

4.判断当前key是否与链表中对应位置的key相等,如果相等的话就用新的value值来覆盖旧的value值并退出循环

Node<K,V> pred = e;
if ((e = e.next) == null) {
    pred.next = new Node<K,V>(hash, key,
                              value, null);
    break;
}

5.上面这个代码就可以看出我们是对当前链表进行的循环,如果循环到了尾节点(next节点为null),都没有找到相同的key,那么就直接使用尾插法,生成一个新的Node节点,然后插入到链表末尾并退出循环

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;
                            }
                        }

6.如果当前头节点是红黑树的节点,则调用红黑树的插入方法;

if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal;
    break;
}

在put结束之后,我们就可以看到最开始定义的binCount的作用了,我们在循环遍历链表的时候每次都记录了次数,因此最后需要判断添加完成之后链表长度是否大于阈值8,如果大于则调用treeifyBin方法进行树化,最后返回oldVal跳出最外层的for循环

 addCount(1L, binCount);

我们发现它在整个循环外层,还调用了这个方法,这个方法的目的也就是让整个长度Size+1,加一之后再判断是否需要进行扩容,若需要则进行扩容操作

扩容机制

这个方法实现特别复杂,我们点进去一步一步来看;

在ConcurrentHashMap中,统计元素总和使用的属性是baseCount

为什么说这个addCount很重要,因为我们ConcurrentHashMap的使用大部分都是在并发场景下,因此我们在解读这个方法的时候要带着并发的思维去进行解读,去观察在这个方法里面它是如何保证线程安全去进行baseCount加一,并且安全进行扩容的

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            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();
        }
    }
}

首先上来就定义了一个CounterCell类型的数组,我们查看一下sumCount这个方法就可以知道了:


    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;
    }

这个方法就是用来统计所有元素数量的,我们可以发现最后返回的sum总数等于CounterCell数组中所有CounterCell对象中的value值相加再加上baseCount值

我们现在再来看CounterCell对象里面有什么:

    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

它内部就只有一个value属性,然后我们现在肯定有很多疑问,那么带着这些疑问再回到addCount方法的代码中;

1.该方法有很多if else分支,第一个分支先判断Cell数组不为空,并且利用CAS对baseCount值加一操作的结果返回取反,也就是说如果Cell数组不为空或者baseCount加一失败了,则进入第一个分支

        再去判断Cell数组是否为空(保证线程安全),如果为空则去计算在Cell数组中的下标值a,a=ThreadLocalRandom.getProbe&m,这个m等于cell数组长度-1,而前面的方法意思是:针对当前线程生成一个随机数,并且将这个随机数保存在内存中,当前线程之后无论调用多少次该方法,得到的值都是和第一次相同的

        如果这个数组不为空,或者计算出来的数组对应下标志a也不为空,那么最后一个判断就是通过CAS对当前数组位置的Cell对象中的value属性执行加一操作的结果进行非运算,如果操作成功了也就是(!uncontended)结果为false才不会走下面的逻辑;若以上三个条件有一个成立了,都说明当前Cell数组是为null的,说明是第一次操作Cell数组,那么就进入到内部调用fullAdddCount方法,进行Cell数组的初始化,并生成Cell对象添加到对应位置

        若没有进入到该方法,说明当前数组不为空,并且CAS添加value值成功了,那么就判断check属性(我们传过来的binCount参数)是否小于等于一,若是则直接返回(说明第一次添加成功,直接返回),如果不是大于等于一,则调用sumCount记录总数赋值给s

3.第三个分支,也就是当Check大于等于0的时候,则在这里面去判断并进行扩容操作;

第二三个分支都比较简单,我们这里主要来看第一个分支,对CounterCell数组是如何操作的:

我们点开fullCount方法

首先我们先看传入这个方法的两个参数xuncontended

第一个参数x:是我们调用addCount方法传入的第一个参数,这里也就是1L

第二个参数uncontended:要想进入到这个方法,uncontended就必须为false了,因此这里传入的也就是false了;

fullCount方法

     int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }

进入到这个方法后,再去获取当前线程的这个随机数,如果为0的话,那么就重新获取一次并赋值给h,再将wasUncontended参数设置为true;

然后紧接着进入到了一个非常长的for循环中

整个for循环的三大分支由下面三个判断组成:

if ((as = counterCells) != null && (n = as.length) > 0) 

 else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1))

 else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))

我们先看后面两个判断逻辑,把最难的留在后面

第二个分支

首先判断条件中出现了一个cellsBusy标记,该标记的意思是表示当前数组是否“暂忙”,也就是当前数组是否有其他线程正在使用,等于0的话表示没有

如果当前数组没有其他线程使用,并且在抵达这个判断的时候,当前数组没有发生改变(没有被其他线程操作过),那么这个时候这个分支就要操作当前数组了,因此通过CAS将cellsBusy标记改为1,并且如果修改成功了,才进入到这个分支中

  boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;

1.确认当前数组依旧没有发生改变,则创建出一个新的Cell数组,并且赋予默认容量2

2.然后生成一个Cell对象,将我们传入的x参数(1L)赋值进去,并且放到数组的对应索引下方(因为我们初始化容量已经为2了,因此不需要再计算Length-1直接使用1就可以了)

3.然后再将外面定义的标记init修改为true即可,最后将cellsBusy标记修改为0表示当前线程操作该数组完毕,最后判断init为true(其他线程没有进入过该方法),则完成,并退出循环

第三个分支

如果前两个对Cell数组的分支操作都不满足,也就是说Cell数组为空,并且有其他线程正在初始化Cell数组往里面添加元素,那么才走到最后这个分支,通过CAS操作来使baseCount值加一;若加一成功了则退出循环;

第一个分支

如果当前Cell数组不为空则进入到第一个分支中来;

     if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);

1.计算出当前数组位置下的元素是否为空,如果为空的话,则再判断cellsBusy标记是否为0,如果为0就生成一个Cell对象,将参数x放进去;

2.再次判断cellsBusy是否0,如果为0则通过CAS操作将其修改为1

3.然后计算出索引下标值,将生成的Cell对象放进去;在finally中将cellsBusy标记修改为1;

4.如果当前索引位置不为空,那么就进入到下面的判断中,判断wasUncontended的非运算结果

5.如果wasUncontended为false,那么就将其修改为true,并调用循环外层的ThreadLocalRandom.advanceProbe方法重新生成一个随机数(与之前的getProbe方法获得的值就不同了),进行第二次循环,回到最开始重新获取一个新的索引位置再操作一次;;

6.那么第二次循环如果还是走到了这里,这个时候wasUncontended为true,那么就会走到下一个判断中去,也就是通过CAS对当前位置的Cell对象中的value值进行加一操作;如果操作成功了就退出循环

7.如果操作失败了,就又会判断另外一个标记collide,若该标记为false,那么将其修改为true,进行下一次的循环,然后再次生成一个新的随机数,回到最开始再重新获取索引位置,再操作一次;

8.如果这一次循环还是走到了这里,这个时候collide为true,那么走到最后一个判断,如果cellsBusy为0,通过CAS将cellsBusy修改为1,执行Cell数组的扩容操作

9.该扩容比较简单,它就是将原来的数组变成了两倍,然后将老数组元素复制过去;

但是我们可以发现,只要collide为ture就会触发扩容,因此在判断前面加了下面这个判断来控制它不能无限制地进行扩容:

也就是满足了上面的判断就会将collide修改为false,然后再次重新获取hash值来循环一次;

这个判断的意思就是,如果当前数组发生了变化(也就是其他线程把我们本来要扩容的这个数组修改了),或者当前数组的大小大于等于CPU的核心数了,那么就不再进行后面的扩容,即把collide修改回false;

通俗易懂的讲就是:第一次进到循环,基于索引判断数组当前位置不为空,那么就会修改wasUncounted标识为true,并且重新计算hash值(调用循环外层的ThreadLocalRandom.advanceProbe方法),进入第二次循环:重新计算索引,如果算出来的位置还是不为空,那么就会去基于CAS进行当前位置value+1的操作,如果添加失败了,那么就会修改collide这个标识为true,再次去重新计算hash值,进入第三次循环:再次重新计算索引,如果算出来的位置还是不为空,然后再次基于CAS进行当前位置的value+1的操作,如果还是失败了,这个时候collide已经为true了,那么经过这几次循环重新计算了两次索引位置并且执行了两次value+1操作,都还没有添加成功,,那么ConcurrentHashMap就会认为当前时间利用率非常低了,那么就会进行扩容用空间利用率去换取时间利用率;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Strine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值