理解JDK8的ConcurrentHashMap底层原理

上文我们提到了ConcurrentHashMap1.7的一些关键知识点,现在我们来了解下ConcurrentHashMap1.8又是怎么实现的呢?
ConcurrentHashMap1.8由于我们的Synchronized优化后,它不在使用1.7的分段锁了而是直接使用Node数组+链表+红黑树的结构,主要利用–> 优化的synchronized + Unsafe(cas)操作实现并发安全。
直接进入主题。

ConcurrentHashMap1.8 put过程是什么样的呢?

1、进来先并发安全的初始化一个容量为16的数组
2、根据key找到对应的数组下标,判断有没有元素,没有就自旋的方式通过cas赋值。
3、然后synchronized加锁,加锁成功。
4、判断类型: value是用volatile修饰,保证了所有线程的可见性。
判断是链表就添加节点到链表。
是红黑树就添加节点到红黑树。
5、根据bigcount个数判断是否要树化
6、进入addcount方法,并发安全的记录count,线程会去竞争baseCount和counterCells数组下的value值,如果没有counterCells数组就会去创建,最后将baseCount和所有value相加统计。
7、同时一个线程发现是ForwardingNode对象,也就是正在扩容则会去帮助扩容。

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;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //线程安全1、进来数组不为空,自旋,通过cas插入内存,旧的预期结果为null
        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
        }
        //两个线程同时扩容,
        else if ((fh = f.hash) == MOVED)
            //帮助扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //线程安全2、链表,插入元素,加锁
            synchronized (f) {
                //重新检查下f是否发送变化,发生变化就重来
                if (tabAt(tab, i) == f) {
                    //链表
                    if (fh >= 0) {
                        binCount = 1;
                        //++binCount记录下链表有多少个元素
                        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;
                            }
                        }
                    }//树
                    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;
                        }
                    }
                }
            }
            
            
            //树化不再锁里面,那其他线程进来put,它正在树化呢,
            //treeifyBin里面加了锁
            if (binCount != 0) {
                //上面是链表的话肯定不成立,binCount个数超过了就变树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //记录下binCount类似size,真正扩容
     addCount(1L, binCount);
     return null;
}

并发安全初始化数组,采用compareAndSwapInt,只有一个线程可以初始化,然后Thread.yield();释放线程

//从主内存拿sizeCtl,默认为0
private transient volatile int sizeCtl;

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
            //多个线程,只允许一个线程更改SIZECTL-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        //取默认初始化容量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-1/4n=0.75n
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //扩容的阈值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

ConcurrentHashMap1.8是如何统计count个数的?

它使用counterCells,进行统计,灵活运用了并发的原理,让统计的工作分配可以使用其他线程进行辅助统计,那我们来看看addcount方法做了什么
上部分主要是counterCells是如何使用并发环境下辅助统计count的,

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
    	//counterCells不是空并且可以修改baseCount值
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //ThreadLocalRandom.getProbe() & m
            //随机数 &length-1
            if (as == null || (m = as.length - 1) < 0 ||
                //a取到随机索引处的CounterCell
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                //cas修改对应CounterCell对象value值
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //修改value失败说明没有CounterCell数组再走这,x是传进来的1L,false
                //compareAndSwapInt(this, CELLSBUSY, 0, 1)
                //cas一个进程,先初始化数组CounterCell,然后value+1
                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();
            }
        }
    }

counterCells部分的代码

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();      // force initialization
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        CounterCell[] as; CounterCell a; int n; long v;
        //起初counterCells为null走最下面if分支
        if ((as = counterCells) != null && (n = as.length) > 0) {
            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;
            }
            //第一次wasUncontended=false,走到最下面进行随机h
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            	//修改value
            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
            //collide=false
            else if (!collide)
                collide = true;
            //collide = true,允许一个进来
            else if (cellsBusy == 0 &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                try {
                    //是否修改
                    if (counterCells == as) {// Expand table unless stale
                        //CounterCell两倍扩容
                        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);
        }//控制一个进行初始化
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {        
                // 检查是否修改
                if (counterCells == as) {
                    //初始化2个
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        //尝试竞争baseCount
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
    }
}

真正扩容的关键代码在这

 //假设stride=2
 //nextIndex=4
 //nextBound=2
 //bound =2
 //i=4-1=3 所以这里是2和3的位置
 
 //那第二次循环就是bound=0,i=1,0和1的位置
 else if (U.compareAndSwapInt
          (this, TRANSFERINDEX, nextIndex,
           nextBound = (nextIndex > stride ?
                        nextIndex - stride : 0))) {
     bound = nextBound;
     i = nextIndex - 1;
     advance = false;
 }

最后我们总结一下1.7和1.8ConcurrentHashMap的知识点

1、ConcurrentHashMap1.7和1.8扩容?

1.7支持多线扩容,前提是segment对象不同,相同的话就会加锁。

扩容条件:(是否大于阈值&数组容量<数组最大容量)2倍扩容
扩容过程–>两倍扩容,遍历旧数组,先遍历到链表的最结尾,根据hash计算出一个索引,把最后面的连续相同索引的节点存入lastrun中直接移到新数组,(有点类似蜘蛛纸牌),然后其他在一个一个转移,头插法添加新的node节点

1.8 支持多线程扩容,扩容性能更好,

扩容条件:新增节点会调用addCount记录元素个数,并检查是否扩容(是否大于阈值&数组不为空&数组容量<数组最大容量)
扩容过程–>a: 根据步长会计算出需要转移的位置,转移过程中间会加锁,生成farwardingNode对象。
b: 转移完成就会向前推进,由又向左,(其他位置没有进行扩容的,可以进行正常的put操作),当自己线程把自己需要转移的元素都转移完了,就会去帮助扩容,如果没有就退出转移过程,进入等待,因为其他进程可能还在扩容,等全部结束了,最后一个扩容就会把新数组赋值给table,成功后就能进行put的操作了。
转移加锁,是链表还是树,都是ln(i)和hn(i+oldCap)分好,然后一次性存入新数组。

2、jdk1.8内部中增加CounterCell来帮助计数。而jdk1.7是通过遍历遍历segment对象计数。

3.1 、每一个Segment都各自加锁,那么在调用Size方法的时候,ConcurrentHashMap1.7怎么解决一致性的问题呢?

ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:
1、遍历所有的Segment。
2、把Segment的元素数量累加起来。
3、把Segment的修改次数累加起来。
4、判断两次是否相同,不同则有修改,重新统计,尝试次数+1;如果不是,说明没有修改,统计结束。如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
5、再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
释放锁,统计结束。

3.2、 为什么这样设计呢?这种思想和乐观锁悲观锁的思想如出一辙。

为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。

4、ConcurrentHashMap1.8为什么用synchronized,cas不是已经保证操作的线程安全吗?

CAS也是适用一些场合的,比如资源竞争小时,是非常适用的,不用进行内核态和用户态之间 的线程上下文切换,同时自旋概率也会大大减少,提升性能,但资源竞争激烈时(比如大量线 程对同一资源进行写和读操作)并不适用,自旋概率会大大增加,从而浪费CPU资源,降低性能。

5、ConcurrentHashMap1.8使用并发环境下统计count的方法总结

addcount里计算count总值,先去竞争basecount,然后有其他进程就会去竞争value值,竞争不到value值得继续去竞争basecount,如果countcell起初没有就初始化,长度为2,最后把basecount和所有value值加起来返回,然后同时addcount里还有判断是否要扩容得方法。

个人学习笔记,

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值