源码分析ConcurrentHashMap(jdk1.7 和jdk1.8)

JDK1.7 ConcurrentHashMap


图示结构

图片来源于https://www.javadoop.com/post/hashmap图片来源于https://www.javadoop.com/post/hashmap]


初始化

ConcurrentHashMap的初始化是个懒加载的过程:实例化ConcurrentHashMap时只新建了Segment数组并初始化了s[0],真正对Segment其他位置的初始化要等到put操作才执行。
initialCapacity:初始容量,这个值时整个ConcurrentHashMap的初始容量,实际操作时要平均分给每个Segment。
loadFactor:负载因子,1.7版本的ConcurrentHashMap和1.7版本的HashMap不同的地方在于Segment数组不可以扩容,所以这个负载因子是给每个Segment内部使用的。

  @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //并发数最大为2^16
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        //ssize为并行级别,保证其为2的n次方
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        //默认情况下concurrencyLevel为16 ,所以sshift为4 ,ssize为16,掩码segmentMask为15,移位数segmentShift为28
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        //数组最大长度2^30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //c默认为1,用于计算每个segment的数组长度。每个segment的长度为不小于c的2的最小次幂
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;

        //每个segment的数组长度,默认值为2,这样扩容阈值为2*0.75=1.5,像segment中插入第一个元素不会扩容,插入第二个则进行扩容
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        //创建 segments和 segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        //往数组中写入s0,其他位置还是null
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

初始化完成后,我们得到一个Segment数组,其中的segment[0]也被初始化了。


put过程

    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
        //1. 首先校验value不能为空
        if (value == null)
            throw new NullPointerException();
        //2. 计算key的hash值
        int hash = hash(key);
        //3. 根据hash值找到Segment数组中的位置j,  j为hash的高四位
        int j = (hash >>> segmentShift) & segmentMask;
        //4. 如果ensureSegment(j)为空,则对 segment[j] 进行初始化
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);//初始化segment[j]
        //5. 将新值插入到槽s[j]中
        return s.put(key, hash, value, false);//进入segment的put方法
    }

	/**
	 * 作用:初始化segment[k]
	 * 过程:循环使用CAS操作进行并发控制
	 */
  	@SuppressWarnings("unchecked")
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //使用ss[0]作为原型
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            //初始化segment[k]内部数组
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            //再次检查该槽是否为空
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                    == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                //使用循环CAS 初始化
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                        == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

	/**
	 * segment的put方法
	 * 方法第一步:获取独占锁
	 */
     final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //在往segment中写入前要先获得该segment的独占锁
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //segment内部数组
                HashEntry<K,V>[] tab = table;
                //再利用hash值求内部数组对应存放位置下标
                int index = (tab.length - 1) & hash;
                //first为数组该位置处的链表表头
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                //替换旧值
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            //没有找到相同的key,则将新结点插入到链表表头
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //如果超过了segment阈值,则需要扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //解锁
                unlock();
            }
            return oldValue;
        }

		/**
		 * tryLock获取锁失败,则通过该方法获得锁。
		 * 该方法有两个出口:1. tryLock方法成功,循环结束   2. 重试次数大于MAX_SCAN_RETRIES,进入lock方法,此方法会阻塞等待,知道成功拿到独占锁,然后break中断循环
		 * 这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。
		 */
       private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node
                            //进入这里,说明数组该位置的链表为空,没有任何元素
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                //重试次数大于MAX_SCAN_RETRIES:单核1,多核64,则不抢了,进入阻塞队列等待
                //lock为阻塞方法,直到获取锁后返回
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                        //当有新的元素进来成为表头,就需要重新走一遍scanAndLockForPut方法
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

rehash过程

Segment数组不能扩容,扩容指的是Segment数组中具体某个位置的小数组 HashEntry<K,V>[] 进行扩容,扩容后大小为原来的2倍。

       /**
         * 对具体segment下的table进行扩容并进行重哈希,然后将新的结点node加入其中
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K, V> node) {
            HashEntry<K, V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //2倍扩容
            int newCapacity = oldCapacity << 1;
            //新的阈值
            threshold = (int) (newCapacity * loadFactor);
            //新的table
            HashEntry<K, V>[] newTable =
                    (HashEntry<K, V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity; i++) {
                //e是链表中的第一个元素
                HashEntry<K, V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K, V> next = e.next;
                    int idx = e.hash & sizeMask;
                    //1. 链表只有一个元素,则直接将结点转移到新table中
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    //2. 链表中不止一个元素
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K, V> lastRun = e;
                        int lastIdx = idx;
                        //2.1 通过for循环会找到一个lastRun结点,该结点以及以后的结点 重新定位的新索引都相同,即该结点之后的所有元素都要放到一起
                        for (HashEntry<K, V> last = next; last != null;last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //2.2 放置lastRun及其之后的所有结点
                        newTable[lastIdx] = lastRun;
                        //2.3 放置链表头结点e和lastRun之间的结点
                        for (HashEntry<K, V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            //头插法插入新结点
                            HashEntry<K, V> n = newTable[k];
                            newTable[k] = new HashEntry<K, V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //3. 所有的元素重哈希后将新来的node放在新的table中相应的链表头部
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

get过程

    public V get(Object key) {
        Segment<K, V> s; // manually integrate access methods to reduce overhead
        HashEntry<K, V>[] tab;
        //1. 获得hash值
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //2. 根据hash值找到对应的segment
        if ((s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) != null &&
                (tab = s.table) != null) {
                //3. 找到segment 内部数组相应位置的链表,遍历
            for (HashEntry<K, V> e = (HashEntry<K, V>) UNSAFE.getObjectVolatile
                    (tab, ((long) (((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

remove过程

    public V remove(Object key) {
        int hash = hash(key);
        Segment<K, V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
    }

	    /**
         * segment的remove方法
         */
       final V remove(Object key, int hash, Object value) {
       		//1. 上锁
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K, V>[] tab = table;
                //2. 由hash值获得具体的数组下标index
                int index = (tab.length - 1) & hash;
                //3.获得对应链表的头部结点
                HashEntry<K, V> e = entryAt(tab, index);
                //4. 使用前驱结点指针
                HashEntry<K, V> pred = null;
                while (e != null) {
                    K k;
                    HashEntry<K, V> next = e.next;
                    if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        if (value == null || value == v || value.equals(v)) {
                        	//要删除的结点是链表头结点,则将该结点的后继结点设为新的头结点
                            if (pred == null)
                                setEntryAt(tab, index, next);
                             //要删除的结点不是链表头结点,则将该结点的前驱结点指向后继结点
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
            	//解锁
                unlock();
            }
            return oldValue;
        }

并发问题分析

由上面的分析可知,put和remove操作都需要给segment加上独占锁,rehash过程是在put操作过程中进行的,也属于获取了独占锁,因此无需考虑它们线程安全问题,然而get操作是不加锁的,因此需要考虑的问题:get的时候在同一个segment中发生了put或者remove操作。

  1. put操作的线程安全性
    1) 初始化槽。在ensureSegment方法中进行,通过循环CAS操作实现,保证了线程安全性。
    2) 添加结点到链表头部。若get操作在链表遍历的过程中已经到了链表中间,则无影响;若get操作发生在put操作之后,需要保证刚插入链表头部的新结点被读取,这主要靠 setEntryAt 方法中使用的 UNSAFE.putOrderedObject 来实现。

    /**
     * Gets the ith element of given table (if nonnull) with volatile
     * read semantics 读语义. Note: This is manually integrated into a few
     * performance-sensitive methods to reduce call overhead.
     */
    @SuppressWarnings("unchecked")
    static final <K, V> HashEntry<K, V> entryAt(HashEntry<K, V>[] tab, int i) {
        return (tab == null) ? null :
                (HashEntry<K, V>) UNSAFE.getObjectVolatile
                        (tab, ((long) i << TSHIFT) + TBASE);
    }
    
    /**
     * Sets the ith element of given table, with volatile write
     * semantics 写语义. (See above about use of putOrderedObject.)
     */
    static final <K, V> void setEntryAt(HashEntry<K, V>[] tab, int i,
                                        HashEntry<K, V> e) {
        UNSAFE.putOrderedObject(tab, ((long) i << TSHIFT) + TBASE, e);
    }
    

    3)rehash。 扩容的过程是新建table数组,然后将旧table元素进行迁移,完成后将table指针指向新table。 若get发生在rehash之前,那么就在旧table上做查询操作;若put发生在rehash之前,那么put操作的可见性保证就是table使用了volatile关键字。

  2. remove操作的线程安全性
    1)如果remove破坏的结点get操作已经遍历过去了,则没有问题;
    2)如果remove先行破坏了结点:1、如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt;2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。

        /**
         * Sets next field with volatile write semantics写语义.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K, V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }
    

JDK1.8 ConcurrentHashMap

和HashMap类似,相较于jdk1.7在jdk1.8中引入了红黑树

图示结构

图片来源于https://www.javadoop.com/post/hashmap图片来源于https://www.javadoop.com/post/hashmap


初始化

//====================先初步分析这两个构造函数,构造函数还有其他...================================
    /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }
    
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        //sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。
        // 如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。
        this.sizeCtl = cap;
    }

put过程

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //得到key的hash值
        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();
            //数组对应位置元素为空,则利用CAS操作将新值放入其中,结束for循环
            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;
                //逻辑到这里,说明f是数组该位置的头结点且不为空
                //插入结点的过程中用到了synchronized保证获取锁
                synchronized (f) {//f = tabAt(tab, i = (n - 1) & hash))
                    if (tabAt(tab, i) == f) {
                        //fh = f.hash   头结点的hash值大于0,说明为链表
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //找到相同的key则进行旧值替换
                                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;
                                //没找到相同的key则将新结点插入到链表的末尾
                                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) {
                    //TREEIFY_THRESHOLD(桶的树形化阈值):8
                    if (binCount >= TREEIFY_THRESHOLD)
                        //同HashMap1.8, 都是要先判断数组长度是否大于最小树形化阈值(默认64),小於则尝试扩容而不是树形化
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }


/**
  * 树形化
  */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //MIN_TREEIFY_CAPACITY(最小树形化阈值):64
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            //b是头结点
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                //加锁
                synchronized (b) {
                    //b为链表头结点
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        //遍历链表,建立一颗红黑树
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //将红黑树设置到数组的相应位置上去
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

put过程中涉及到了初始化数组扩容帮助数据迁移三个重要的过程,下面将一一介绍。


初始化数组

该方法主要就是初始化一个合适大小的数组,然后会设置sizeCtl。
初始化方法中的并发问题是通过对sizeCtl进行一个CAS操作来完成的。

   /**
     * 初始化数组
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //sizeCtl小于0,说明有其他线程正在对table进行初始化或者扩容,使用yield进行等待
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //逻辑到这里,说明sizeCtl>=0,CAS将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;//DEFAULT_CAPACITY,默认数组长度为16
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //table是volatile的
                        table = tab = nt;
                        //sc=0.75 * n;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //将sc赋给sizeCtl
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }


扩容

扩容操作tryPresize中涉及到了数据迁移transfer方法,这两个方法也是 Java8 ConcurrentHashMap中比较复杂的方法

	// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
    private final void tryPresize(int size) {
        //c: 【1.5倍的size再加1,再往上取最近的2的n次方】
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //这个if分支和初始化数组的代码基本一致,可以略过
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                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 - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //数组长度大于允许的最大长度2^30,或者扩容的新长度<sc,则什么都不做,直接返回
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            //下面的过程用到了数据迁移transfer
            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;
                    // 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法
                    //    此时 nextTab 不为 null
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2),此时nextTab 为null
                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 源码才清楚。


数据迁移

下面这个方法有点长,将原来的 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 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程,这个读者应该能理解吧。其实就是将一个大的迁移任务分为了一个个任务包。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;

    // stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
    // stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
    //   将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    // 如果 nextTab 为 null,先进行一次初始化
    //    前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
    //       之后参与迁移的线程调用此方法时,nextTab 不会为 null
    if (nextTab == null) {
        try {
            // 容量翻倍
            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 是 ConcurrentHashMap 中的属性
        nextTable = nextTab;
        // transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
        transferIndex = n;
    }

    int nextn = nextTab.length;

    // ForwardingNode 翻译过来就是正在被迁移的 Node
    // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
    // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
    //    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
    //    所以它其实相当于是一个标志。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);


    // advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab

    /*
     * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
     * 
     */

    // i 是位置索引,bound 是边界,注意是从后往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 下面这个 while 真的是不好理解
        // advance 为 true 表示可以进行下一个位置的迁移了
        //   简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;

            // 将 transferIndex 值赋给 nextIndex
            // 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的迁移操作已经完成
                nextTable = null;
                // 将新的 nextTab 赋值给 table 属性,完成迁移
                table = nextTab;
                // 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }

            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            // 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任务结束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;

                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 头结点的 hash 大于 0,说明是链表的 Node 节点
                    if (fh >= 0) {
                        // 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
                        // 需要将链表一分为二,
                        //   找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
                        //   lastRun 之前的节点需要进行克隆,然后分到两个链表中
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 其中的一个链表放在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 另一个链表放在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        // 红黑树的迁移
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;

                        // 将 ln 放置在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 将 hn 放置在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }
                }
            }
        }
    }
}

transfer方法并没有实现所有的迁移任务,每次调用这个方法只是实现了transferIndex往前stride个位置的迁移工作,其他的需要由外围来控制,即tryPrize方法。


get过程

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //1. 获得hash值
        int h = spread(key.hashCode());
        //2. 根据hash值定位到数组具体位置
        //2.1  具体位置为null,则直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //2.2 数组对应位置正好是要找的元素,则直接返回该元素
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //2.3 对应位置hash值小于0,说明正在扩容或者是红黑树,继续调用find方法查找,  参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)

            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //2.4 对应位置hash值正常,则遍历链表查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }


总结

JDK1.7ConcurrentHashMap:

  • 初始化: 懒加载,创建了一个Segment数组并初始化了Segment[0]
  • put过程:
  1. 校验value不能为空,为空则直接抛空指针异常 key可以空 !!1.8版本key和value都不能为空
  2. 根据key得到hash值,然后根据hash值找到Segment数组中的位置j
  3. 若Segment[j]为null,则根据Segment[0]为原型对Segment[j]进行初始化,初始化的过程中使用循环CAS确保线程安全
  4. 在Segment[j]中将新元素插入到合适位置,这个过程首先要获得独占锁,Segment 直接继承了 ReentrantLock,所以可以直接调用tryLock方法获得锁。由于Segment的数据结构就是数组+链表,所以插入过程和JDK1.7的HashMap很像,比较大的区别就是ConcurrentHashMap通过获取独占锁保证了线程安全。
  • get过程:
  1. 先根据key的hash值找到对应的Segment
  2. 再根据 hash 找到数组中具体的位置
  3. 到这里是链表了,顺着链表进行查找即可
  • 扩容过程rehash:
    扩容时机:当向某个Segment中插入具体元素前,发现如果插入元素后这个Segment的元素超过了阈值且元素数量少于允许的最大容量,则先针对这个Segment进行扩容,然后再插入新元素。因此,扩容是针对Segment的扩容
  1. 创建旧table长度2倍大小的新数组
  2. 重新计算原数组每个元素在新数组中的位置
    2.1 如果旧数组对应位置只有一个元素,即链表只有一个结点,则直接将元素放在新数组中;
    2.2 否则对每个链表,从链表中找到一个lastRun节点,这个节点之后的所有元素和这个节点在新数组的下标相同
    2.2.1 将 lastRun 及其之后的所有节点组成的这个链表放到 新数组对应的下标位置上
    2.2.2 将lastRun之前的结点一一利用头插法插入到新数组对应的下标位置上
  3. 旧table的所有元素重哈希迁移完毕后,新的结点根据hash值计算在新数组具体的下标位置,然后插入到对应的链表头部。

JDK1.8ConcurrentHashMap: 相比较1.7引入了红黑树

  • 初始化:懒加载,在第一次put操作时才会真正初始化数组。通过CAS操作控制sizeCtl来保证线程的安全性。当sizeCtl<0时,说明有其他线程正在初始化或者正在扩容,调用Thread.yield()方法让给其他线程;当sizeCtl>=0时则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;
  • put过程:
  1. key和value都不能为空,为空则抛空指针异常
  2. 根据key得到hash值
  3. 数组为空则进行初始化
  4. 找hash值对应的数组下标,得到第一个结点f
    3.1 如果数组对应位置为空即f为null,则直接CAS将新值放入其中即可
    3.2 如果f.hash等于-1,说明正在扩容则帮助数据迁移
    3.3 上述两种情况都不满足则使用synchronized获取数组该位置的头结点的监视器锁
    3.3.1 如果f是链表结点,则找到相同的key则进行旧值覆盖;找不到则将新值结点加到链表末尾
    3.3.2 如果f是树结点,则调用红黑树的插值方法插入新节点
  5. 插入后根据桶的树形化阈值判断是否需要树形化(在树形化 中的第一步是判断数组长度是否达到最小树形化阈值,若没有则扩容而不是树形化)
  • get过程:
  1. 计算 hash 值
  2. 根据 hash 值找到数组对应位置: (n - 1) & h
  3. 根据该位置处结点性质进行相应查找
    3.1 如果该位置为 null,那么直接返回 null 就可以了
    3.2 如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
    3.3 如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,调用相应的find方法查找
    3.4 如果以上 3 条都不满足,那就是链表,进行遍历比对即可
  • 扩容过程:翻倍扩容,扩容后数组容量为原来的 2 倍。
  • 数据迁移:原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了。Doug Lea使用了一个stride,可以理解为步长,每个线程每次负责迁移一部分。第一个发起数据迁移的线程将transferIndex指向原数组最后的位置,然后从后往前 的stride个任务属于第一个线程,再往前stride个任务属于第二个线程,以此类推。思想就是将一个大的迁移任务分为了一个个任务包。

参考

https://www.javadoop.com/post/hashmap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章