Java集合框架源码阅读——ConcurrentHashMap

目录

一、Map的初始化

二、插入数据

三、元素计数

四、Map的扩容


一、Map的初始化

map初始化的方法为initTable(),使用CAS方法保证只有一个线程来对map初始化。最外层的while循环判断map是否仍未空,不为空说明已经有线程完成初始化了,直接返回。

之后再根据sizeCtl的值判断当前是否有线程正在初始化。sizeCtl是一个成员变量,可能出现三种情况:

  • > 0,说明map没有正在初始化或扩容,这时的值表示map的容量阈值
  • == -1,说明map正在进行初始化
  • < -1,说明map正在进行扩容,值为一个很小的负数,可以计算出有几个线程正在帮助扩容

线程根据sizeCtl的值决定有没有并发冲突,使用CAS将sizeCtl值为1表示自己获得了控制权(相当于获得了锁,但开销比悲观锁小)。

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    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
            // 让这个线程对map进行初始化,将sc置为-1
            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;
    }

compareAndSwapInt()方法的四个参数为:

  • 要改变的对象
  • 要改变的字段在第一个参数中的偏移量
  • 期待看到的值(比较的值、修改前的值)
  • 要交换的值(修改后的值)

根据调用时传入的参数可知,首先判断要sizeCtl的值与之前一样,保证在这期间没有线程开始初始化。之后将sizeCtl的值置为-1,让自己获得初始化的控制权。以上这一比较与交换过程由操作系统保证原子性。SIZECTL时sizeCtl属性在ConcurrentHashMap中的偏移量,这是Unsafe对象U算出来的。

二、插入数据

在putVal()方法的执行流程为:

  1. 判断当前hashmap是否为空,为空则先去初始化
  2. 判断要插入的bin是否为空,为空则使用CAS插入数据
  3. 判断hashmap是否正在扩容(bin的首节点的hash值为-1),若正在扩容则取帮助扩容
  4. 对这个bin加锁,完成插入操作

其中在对于bin加锁之后,不存在线程安全的问题,因此只需考虑casTabAt()、initTable()、helpTransfer()、addCount()四个方法的并发设计

final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key和value不许为null,与HashTable一样,与HashMap不一样
        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;
            // 情况1:初始化
            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, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 情况3:帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 情况4:加锁插入
                synchronized (f) {
                    // ... 插入数据 ...
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 最后:binCount + 1
        addCount(1L, binCount);
        return null;
    }

casTabAt()方法中只有一条语句,与初始化时设置sizeCtl相似,也是使用CAS的方法保证线程安全地插入数据

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

三、元素计数

ConcurrentHashMap中没有直接维护size变量来记录当前map中存储的元素数量,而是存储了一个CounterCell数组,名为CounterCells,以及一个baseCount变量。每一个CounterCell对象中存储一个数值,需要计算总元素个数时,使用sumCount()方法将所有的CounterCell中的数值和baseCount的值求和。这样做的优点是出现并发操作之前,直接将baseCount+1就可以了,有并了发操作后,每个线程可以互不影响地单独修改一个CounterCell的值(用过CounterCell后就不再用baseCount了,但有一种情况除外)。

如何把一个线程分配到一个CounterCell?使用ThreadLocalRandom.getProbe()获得线程随机数,对CouterCells的长度取模。

     /**
     * A padded cell for distributing counts.  Adapted from LongAdder
     * and Striped64.  See their internal docs for explanation.
     */
    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

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

在putVal()方法的最后,调用了addCount()方法将元素数量+1,addCount()方法的执行流程为:

  1. 判断是要修改baseCount还是CounterCell。如果目前都没有出现过并发,且CAS能直接修改baseCount,则直接修改
  2. 如果需要修改CounterCell,使用CAS修改CounterCell,对于以下几种特殊情况,调用fullAddCount()方法继续完成操作:(1)CounterCells为空;(2)要修改的CounterCells中的元素为空;(3)CAS修改CounterCell失败
  3. 检查是否需要对map扩容
 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) {
            // 判断是否需要扩容
        }
}

fullAddCount()方法在上述几种特殊情况下保证继续对CounterCell完成操作,对于上面说的第一种情况,CounterCells数组为空,使用CAS尝试获取锁,若能获取到,当前线程完成对数组的初始化。

else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 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;
            }

若不能获取到,说明当前有其它线程正在初始化CounterCells数组,这个线程也不等了,直接使用CAS修改操作baseCount。

else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base

对于上面的第二种情况,要修改的CounterCell对象为空,也是使用CAS获取锁,初始化这个位置上的CounterCell对象。

if ((as = counterCells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        // 先创建一个CounterCell对象
                        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;
                }
                // 其它情况
                .....

四、Map的扩容

ConcurrentHashMap的扩容可以由多个线程共同完成,每个线程负责迁移map中的一部分(负责范围 = stride)。扩容的过程从后向前,每个线程使用变量 i 记录当前准备扩容的bin,使用bound表示目前需要负责的最小bin的下标。当一个线程已经将自己要负责的部分全部迁移完成后,会继续从transferIndex开始,向前认领另一分部(范围仍然 = stride)。可见,transferIndex会被多个线程修改,因此对于transferIndex的修改使用CAS完成。

在for循环的开始部分,当前线程先要确认自己这次要迁移的bin的下标和自己的bound(while循环),advance变量表示当前线程是否完成了对bin[i]的迁移,即是否需要向前移动。当自己当前的任务已经完成时,可能要去认领新的任务。

ForwardingNode是一个静态内部类,一个bin的位置完成迁移后,将这个bin的第一个节点赋值为ForwardingNode类型,用来标识已经完成迁移的部分

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 计算stride大小,最小为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        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 (advance) {
                int nextIndex, nextBound;
                // 已经完成扩容,或自己的任务还没完成
                if (--i >= bound || finishing)
                    advance = false;
                // 自己的任务完成了,但也不需要再领取新任务了
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 从transferIndex开始向前认领stride部分的扩容任务,使用CAS修改transferIndex
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }

根据刚刚确定的 i 的值,判断是否已经完成扩容。

if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 更新数组的引用
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 使用CAS修改sizeCtl的值 - 1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }

若bin[i]的位置为空,说明不用迁移,直接将这个位置的节点设置为ForwardingNode表示完成迁移。若不为空,则对bin[i]加锁,完成数据的迁移。

else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                // 。。。
                }
            }
// 。。。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值