Hello World

ConcurrentHashMap

传送门 : Java7/8中ConcurrentHashMap全解析

1.基本属性

  元素存储在Node类型的table[]中

  线程安全通过Synchronzied和CAS操作实现

1.1 sizeCtl

  • -1代表正在初始化
  • -N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。

     当表示阈值时,只等于当前容量的1.5倍加1

    // 返回一个大于等于且最接近 c 的2的幂次方整数
    private static final int tableSizeFor(int c) {
        int n = c - 1;                                                                    1001 1001 1001 1001
        n |= n >>> 1; // 将第一个1后面的值也变为1, 现在(第一个高位不为0的位置开始)从最高位开始,后面的2位都是1   将最高位(包括最高位)后面1位变为1
        n |= n >>> 2; // 将前面2位为1的后面2位也变为1, 现在从最高位开始,后面4位都是1    将最高位(包括最高位)后面2位变为1
        n |= n >>> 4; // 将前面4位为1的后面4位也变为1, 现在从最高位开始,后面8位都是1    将最高位(包括最高位)后面4位变为1
        n |= n >>> 8; // 将前面8位为1的后面8位也变为1, 现在从最高位开始,后面16位都是1    将最高位(包括最高位)后面8位变为1
        n |= n >>> 16;// 将前面16位为1的后面16位也变为1, 现在从最高位开始,后面32位都是1    将最高位(包括最高位)后面16位变为1
                      // 上面的操作就是将最高位后面的所有位变为1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

2. initTable()

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            // sizeCtl < 0 表示有其他线程在进行初始化操作或扩容操作
            if ((sc = sizeCtl) < 0)
                Thread.yield(); 
            // 将 sizeCtl 置为 -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 * 0.75
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

 

3. put()     

final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key  value 都不允许为 null
        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();
            // 计算节点在数组中的存储位置
            // 存储位置的头节点是否为空
            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;                   
            }
            // 表示有其他线程在进行扩容

        // 在 Node 的子类 ForwardingNode 的构造方法中,可以看到这个变量作为 hash 值进行了初始化。
        // 而这个构造方法只在一个地方调用了,即 transfer(扩容) 方法。

            else if ((fh = f.hash) == MOVED)
                // 帮助其他线程扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 对头节点加锁
                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;
                                    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;
                            }
                        }
                    }
                }
                
                if (binCount != 0) {        
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 数组长度 < 64时扩容数组, 否则转换为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

 4. tryPresize() 扩容

    private final void tryPresize(int size) {
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            // 返回一个大于等于且最接近 (size + (size >>> 1) + 1) 的2的幂次方整数
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        // sizeCtl >= 0 表示没有其他线程进行初始化或者扩容操作
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            // 数组尚未初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                // sizeCtl 置为 -1 表示在进行初始化操作
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            // 等价于 sc = n * 0.75  n总是2^n, 右移两位将相当于 n * 1/4 
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            // 计算出的扩容后的容量 < sizeCtl, 或容量大于最大容量
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    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);
            }
        }
    }

  这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。

  所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚。

5.  transfer() 数据迁移

  下面这个方法很点长,将原来的 tab 数组的元素迁移到新的 nextTab 数组中。

  虽然我们之前说的 tryPresize 方法中多次调用 transfer 不涉及多线程,但是这个 transfer 方法可以在其他地方被调用,典型地,我们之前在说 put 方法的时候就说过了,请往上看 put 方法,是不是有个地方调用了 helpTransfer 方法,helpTransfer 方法会调用 transfer 方法的。

  此方法支持多线程执行,外围调用此方法的时候,会保证第一个发起数据迁移的线程,nextTab 参数为 null,之后再调用此方法的时候,nextTab 不会为 null。

  阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 Doug Lea 使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。

  第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程,这个读者应该能理解吧。其实就是将一个大的迁移任务分为了一个个任务包。

posted @ 2018-08-20 00:28  小小忧愁米粒大  阅读(138)  评论(0编辑  收藏  举报
瞅啥瞅,好好看书