深入分析CocurrentHashMap源码

Java8 ConcurrentHashMap数据结构

与HashMap相同,Java8中,CocurrentHashMap也采用了数组+链表+红黑树的结构存储数据。基本的数据特征,如capacity、threshold、fator等与HashMap保持一致,这在一定程度上降低了学习成本。下面的学习过程中,也会与HashMap进行比对分析,以便更好的理解CocurrentHashMap为了保证线程安全性所做的处理。
结构图网上已有很多,直接借鉴了一份:
ConcurrentHashMap结构图

初始化及常量

常量

	private static final int MAXIMUM_CAPACITY = 1 << 30;
    private static final int DEFAULT_CAPACITY = 16;
    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;
	//以上与HashMap相同,不解释
	
    // 最小步长,参考下面transfer方法说明
    private static final int MIN_TRANSFER_STRIDE = 16;

    /**
     * The number of bits used for generation stamp in sizeCtl.
     * Must be at least 6 for 32bit arrays.
     */
    private static final int RESIZE_STAMP_BITS = 16;

    /**
     * The maximum number of threads that can help resize.
     * Must fit in 32 - RESIZE_STAMP_BITS bits.
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    /**
     * The bit shift for recording size stamp in sizeCtl.
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    // 数据迁移状态下的特殊节点标识,resize table时产生
    static final int MOVED     = -1; // hash for forwarding nodes
    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数,当前版本只影响transfer stride计算
    static final int NCPU = Runtime.getRuntime().availableProcessors();

末尾处的常量,主要用于CAS,这里不展开讨论Unsafe。

	// Unsafe mechanics
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long SIZECTL
        = U.objectFieldOffset(ConcurrentHashMap.class, "sizeCtl");
    ...

初始化
无线程安全操作,忽略。

put方法

对于线程安全的对象设计,改变数据内容的方法是首先要考虑的对象,如添加、修改、删除等。put方法是HashMap的基本写入方法,其它写入方法都直接或间接使用了put。以put为入口进行分析,基本涵盖了ConcurrentHashMap的主要线程安全处理机制。
put只是简单的接口封装,真正的实现在putVal方法:

	final V putVal(K key, V value, boolean onlyIfAbsent) {
		// 0 准备阶段
        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; K fk; V fv;
            // 1 初始化table
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 2 桶所在位置没有元素,cas直接插入
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            // 3 正在进行数据迁移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 4 absent处理
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            // 5 针对链表或红黑树进行put操作
            else {
            	V oldVal = null;
                synchronized (f) {
                    ... //忽略此处代码,在5位置说明
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

0 准备阶段
计算hash值。spread方法将高位与低位进行xor运算,使hash值的高位部分有更多机会参与比较,目的应该是减少碰撞。是否有效与数据特征有关,这里不展开。
1 初始化table
如果table尚未初始化,初始化table。使用CAS机制保证线程安全。
关键代码

		while ((tab = table) == null || tab.length == 0) {
			if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {

熟悉CAS略过。基本原理是利用U的原子操作进行比对和赋值,竞争获胜的线程将sizeCtl改为-1,并对table进行初始化,初始化完成后将sizeCtl赋值为新的值。其它线程在sizeCtl < 0期间处于等待状态,直到table初始化完成,结束while循环退出。
2 桶所在位置没有元素,CAS直接插入
同理,利用CAS机制保证只有一个线程实现写入操作,竞争失败者进入自身的下一次循环,此时判断条件table首节点不为空,进入后面的条件分支。
3 正在进行数据迁移
table首节点处于MOVED状态,表示CocurrentHashMap正处于数据迁移中,当前线程调用helpTransfer帮助进行数据迁移。数据迁移的细节在下面transfer方法中展开说明。
4 absent处理
已有元素,直接返回。
5 针对链表或红黑树进行put操作
插入操作全部代码由synchronized(f),以头节点为对象锁进行同步,保证线程安全。
插入操作完成后,可能会涉及链表->红黑树的转换,同样使用synchronized实现同步。

addCount

这里需要单独说明一下putAll方法最后一行代码addCount。元素插入完成后需要对ConcurrentHashMap中的元素个数进行增加操作,此时有可能引起扩容。对于HashMap来说,扩容通过resize方法实现。对于ConcurrentHashMap来说,扩容操作在这个addCount方法里发生,通过transfer实现。
对于addCount的并发,采用了与LongAdder相同的机制,通过CounterCell数组及CAS控制计算,有兴趣的同学可以参考LongAdder的相关分析。

	private final void addCount(long x, int check) {
        CounterCell[] cs; long b, s;
        if ((cs = counterCells) != null ||
            !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell c; long v; int m;
            boolean uncontended = true;
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            //如果有并发的addCount发生(包括历史上发生过),将通过sumCount计算元素个数
            //s = baseCount + 所有CounterCell里面的数值
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 满足扩容条件,调用transfer进行扩容操作
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
                if (sc < 0) {
                    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt); //开始数据迁移
                }
                else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                    transfer(tab, null); //开始数据迁移
                s = sumCount();
            }
        }
    }

transfer及helpTransfer

transfer方法进行数据迁移操作。基本的操作单元是table的一个节点,可能是一个元素、链表、红黑树。每完成一次迁移,旧table的对应位置置为ForwardingNode,以此通知其它线程。
这里需要单独说明一下stride,一个步长的概念。这个步长指的是线程间隔的步长,意思是告诉并发进来的线程,你离我stride这么远去做迁移吧,咱俩互不干扰。stride = (NCPU > 1) ? (n >>> 3) / NCPU : n,最小为MIN_TRANSFER_STRIDE(16),与cpu数相关,若cpu为1,则不进行并发,因为只有一个cpu情况下,多个线程同时参与数据迁移并不能带来效率提升。

	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
        // 初始化新table
        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;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // while循环体内,计算开始数据迁移的位置
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSetInt
                         (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;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 空元素,仅修改原table对应位置状态,无需数据迁移
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 满足这个条件,说明有其它线程已经对这个位置进行了数据迁移
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            // 对当前i位置的数据进行迁移
            else {
                synchronized (f) {
                    ... // 具体迁移代码忽略
                }
            }
        }
    }

helpTransfer方法
参照put方法中第3步说明,如果插入数据时发现其它线程正在进行数据迁移,则当前线程参与数据迁移操作。helpTransfer方法中经过一定条件判断,最终调用transfer参与数据迁移,此处不展开说明了。

get方法

get方法与HashMap大同小异,全程没有同步控制需求,唯一需要关注的点

			else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;

put方法中正在进行数据迁移时,MOVED状态的节点会进入这个分支,即已经迁移的数据通过ForwardingNode节点到对应的新table中进行查找。

总结

至此,CocurrentHashMap的线程安全机制基本分析完毕。回顾一下,其设计的精华在于transfer方法。
首先,通过ForwardingNode将同步的粒度巧妙降低到Node节点,大大提高了并发的执行效率。ForwardingNode机制保证了在数据迁移期间,已经迁移到新表的数据仍然可以get到。ForwardingNode同时告知其它put数据的线程,插入点已经迁移到新表,请等待数据迁移完毕再进行put;若在多cpu环境下,等待期间可以帮我进行数据迁移。
其次,stribe步长的设计充分考虑到cpu的利用率,实现同步效率的最大化。单cpu场景下,多线程执行数据迁移只能增加线程切换开销,无助于效率提升,因此步长设计为>=table.length,只有一个线程进行数据迁移,其它线程处于等待状态。多cpu场景下,不同线程间间隔stribe距离同时进行数据迁移,减少了互相干扰的次数。
此外,AddCount方法中的CounterCell机制也值得借鉴,这个可以参考LongAdder的相关分析。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值