Java并发—ConcurrentHashMap

Java并发容器—ConcurrentHashMap

1.JDK1.7版本

​ jdk1.7的实现结构图如下所示

[外链图片转存失败(img-Fy93gKVd-1566009849146)(/home/shidongxuan/.config/Typora/typora-user-images/1566002052129.png)]

CurrentHashMap是由Segment数组和HashEntry数组结构组成. Segment是一种可重入锁(ReentrantLock), 在ConcurrentHashMap里扮演锁的角色, HashEntry用于存储键值对数据. 综上, ConcurrentHashMap采用了分段锁技术, 其中Segment继承与ReentrantLock. 不像HashTable那样不管是put操作还是get操作都需要做同步处理, 理论上ConcurrentHashMap支持段级别的线程并发, 当一个线程占用锁访问一个Segment时, 不会影响其他Segment

1>put方法
	//先定位到Segment, 之后在对应的Segment上进行具体的put
	public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
	//具体的put
	final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //先尝试对segment加锁, 如果加锁成功, 那么node=null,如果加锁失败则调用
        	//scanAndLockForPut方法去获取锁, 这个方法中, 获取锁后会返回对应的HashEntry 
        	HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //优化点, 这时已经加了锁,只会有一个线程访问table, 但是table是volatile修饰的
                //所以将其赋值给一个局部变量, 减少volatile带来的开销
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                //获取到对应位置上的HashEntry链表
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        //e不为空, 说明出现了hash冲突, 遍历当前链表
                        //如果找到一个key相同的, 则根据onlyIfAbsent判断是否覆盖value值
                        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 {
                        //说明e为空, 一种可能是在上面遍历到最后没有找到key相同的, 也可能是一开始就是null
                        //第一次没有获取到锁, 通过scanAndLockForPut方法获取到了锁,放到敌营位置上
                        if (node != null)
                            node.setNext(first);
                        else
                            //否则新new出一个节点对象
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;//节点数+1
                        //判断是否需要扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            //数组无须扩容, 就直接插入node到指定index位置
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

下面是获取锁的scanAndLockForPut方法

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
    //如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,
    //这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,
    //这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //获取锁失败,初始时retries=-1必然开始先进入第一个if
        if (retries < 0) {//<1>
            if (e == null) { //<1.1>
                //e=null代表两种意思,第一种就是遍历链表到了最后,仍然没有发现指定key的entry;
                //第二种情况是刚开始时确实太过entryForHash找到的HashEntry就是空的,即通过hash找到的table中对应位置链表为空
                //当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,
                //然后进行循环尝试获取锁,在循环次数还未达到<2>以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,
                //那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,
                //所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))//<1.2>   遍历过程发现链表中找到了我们需要的key的坑位
                retries = 0;
            else//<1.3>   当前位置对应的key不是我们需要的,遍历下一个
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {//<2>
            // 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁,
            //之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对系统性能有消耗的,
            //这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {//<3>
            // 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

其核心思想就是通过MAX_SCAN_RETRIES控制自旋次数, 防止无限制的自旋和浪费资源. 这个方法的作用就是遍历获取所然后进行数据插入

2>rehash方法
/**
 * Doubles size of table and repacks entries, also adding the
 * given node to new table
 * 对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,
 * 对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点fv,
 * 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=fv,即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中
 * 这样最好的情况是所有oldTable中对应头结点后跟随的节点在newTable中的新的index均和头结点一致,那么就不需要创建新节点,直接复用即可。
 * 最坏情况当然就是所有节点的新的index全部发生了变化,那么就全部需要重新依据k,v创建新对象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Single node on list 只有单个节点
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }//这个for循环就是找到第一个后续节点新的index不变的节点。
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                //第一个后续节点新index不变节点前所有节点均需要重新创建分配。
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}
3>ensureSegment方法
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) {
        Segment<K,V> proto = ss[0]; // 以初始化时创建的第一个坑位的ss[0]作为模版进行创建
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 二次检查是否有其它线程创建了这个Segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                //这里通过自旋的CAS方式对segments数组中偏移量为u位置设置值为s,这是一种不加锁的方式,
                //万一有多个线程同时执行这一步,那么只会有一个成功,而其它线程在看到第一个执行成功的线程结果后
                //会获取到最新的数据从而发现需要更新的坑位已经不为空了,那么就跳出while循环并返回最新的seg
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

这个方法核心是利用自旋CAS来创建对应的Segment, 这种思想是不加锁保证线程安全.

4>get方法
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);//获取key对应hash值
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//获取对应h值存储所在segments数组中内存偏移量
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //通过Unsafe中的getObjectVolatile方法进行volatile语义的读,获取到segments在偏移量为u位置的分段Segment,
        //并且分段Segment中对应table数组不为空
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {//获取h对应这个分段中偏移量为xxx下的HashEntry的链表头结点,然后对链表进行 遍历
            //###这里第一次初始化通过getObjectVolatile获取HashEntry时,获取到的是主存中最新的数据,但是在后续遍历过程中,有可能数据被其它线程修改
            //从而导致其实这里最终返回的可能是过时的数据,所以这里就是ConcurrentHashMap所谓的弱一致性的体现,containsKey方法也一样
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

这里这个方法是弱一致性的, 所以可能会返回过时的数据

5>size方法
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // 是否溢出
    long sum;         // 存储本次循环过程中计算得到的modCount的值
    long last = 0L;   // 存储上一次遍历过程中计算得到的modCount的和
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {//无限for循环,结束条件就是任意前后两次遍历过程中modcount值的和是一样的,说明第二次遍历没有做任何变化
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        //由于只有在retries等于RETRIES_BEFORE_LOCK时才会执行强制加锁,并且由于是用的retries++,
        //所以强制加锁完毕后,retries的值是一定会大于RETRIES_BEFORE_LOCK的,
        //这样就防止正常遍历而没进行加锁时进行锁释放的情况
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

如果要统计整个ConcurrentHashMap中的元素个数, 将每个Segment中的元素数量加起来就好了, 正好Segment中的count是一个volatile变量. 但如果在累加的时候, 其他线程进行了插入或删除操作, 结果就不准了. 所以最安全的方法是对所有Segment的put, remove, clean方法锁住, 但是这种方法很低效.

因为在累加count的过程中, 之前累加过的count发生变化的几率很小, 所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment的大小, 如果统计过程中count发生了变化, 则再采用加锁的方式统计所有Segment的大小

2.JDK1.8版本

在jdk1.8中, ConcurrentHashMap通过一个Node<K,V>[]数组来保存添加到map中的键值对, 在同一个数组位置是通过链表或红黑树的形式来表示, 其实原理类似于jdk1.8中的HashMap

1>put方法
	public V put(K key, V value) {
        return putVal(key, value, false);
    }
    /*
     * 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组
     *  然后通过计算hash值来确定放在数组的哪个位置
     * 如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来
     * 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
     * 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
     *    然后判断当前取出的节点位置存放的是链表还是树
     *    如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
     *          则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
     *    如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
     *  最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
     *  则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
        int hash = spread(key.hashCode());    //取得key的hash值
        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();    //第一次put的时候table没有初始化,则初始化table
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    /
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))        //创建一个Node添加到数组中区,null表示的是下一个节点为空
                    break;                   // no lock when adding to empty bin
            }
            /*
             * 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
             * 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
             */
            else if ((fh = f.hash) == MOVED)    
                tab = helpTransfer(tab, f);
            else {
                /*
                 * 如果在这个位置有元素的话,就采用synchronized的方式加锁,
                 *     如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
                 *         如果找到了key和key的hash值都一样的节点,则把它的值替换到
                 *         如果没找到的话,则添加在链表的最后面
                 *  否则,是树的话,则调用putTreeVal方法添加到树中去
                 *  
                 *  在添加完之后,会对该节点上关联的的数目进行判断,
                 *  如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
                 */
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {   //再次取出要存储的位置的元素,跟前面取出来的比较
                        if (fh >= 0) {  //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
                            binCount = 1;            
                            for (Node<K,V> e = f;; ++binCount) {    //遍历这个链表
                                K ek;
                                if (e.hash == hash &&        //要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)        //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {    //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
                                    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,    //调用putTreeVal方法,将该元素添加到树中去
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)    //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
                        treeifyBin(tab, i);    
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);    //计数
        return null;
    }
2>扩容transfer方法
	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
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  // 构建一个nextTable,大小为table两倍
                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自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点;
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) { // 遍历table中的每一个节点 
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (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赋值给table 清空临时对象nextTable  
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);  //扩容阈值设置为原来容量的1.5倍  依然相当于现在容量的0.75倍
                    return;
                }
                // 利用CAS方法更新这个扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
                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
                }
            }
            //如果遍历到的节点为空 则放入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;
                        if (fh >= 0) {  // 链表节点
                            int runBit = fh & n;  // resize后的元素要么在原地,要么移动n位(n为原capacity),详解见:https://huanglei.rocks/coding/194.html#4%20resize()%E7%9A%84%E5%AE%9E%E7%8E%B0
                            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);
                            }
                            //在nextTable的i位置上插入一个链表 
                            setTabAt(nextTab, i, ln);
                            //在nextTable的i+n的位置上插入另一个链表
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            //设置advance为true 返回到上面的while循环中 就可以执行i--操作

                            advance = true;
                        }
                        //对TreeBin对象进行处理  与上面的过程类似 
                        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;
                                }
                            }
                            // (1)如果lo链表的元素个数小于等于UNTREEIFY_THRESHOLD,默认为6,则通过untreeify方法把树节点链表转化成普通节点链表;(2)否则判断hi链表中的元素个数是否等于0:如果等于0,表示lo链表中包含了所有原始节点,则设置原始红黑树给ln,否则根据lo链表重新构造红黑树。
                            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;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd); // tab[i]已经处理完了
                            advance = true;
                        }
                    }
                }
            }
        }
    }

如何在扩容时,并发地复制与插入?

  1. 遍历整个table,当前节点为空,则采用CAS的方式在当前位置放入fwd
  2. 当前节点已经为fwd(with hash field “MOVED”),则已经有有线程处理完了了,直接跳过 ,这里是控制并发扩容的核心
  3. 当前节点为链表节点或红黑树,重新计算链表节点的hash值,移动到nextTable相应的位置(构建了一个反序链表和顺序链表,分别放置在i和i+n的位置上)。移动完成后,用Unsafe.putObjectVolatile在tab的原位置赋为为fwd, 表示当前节点已经完成扩容。
3>get方法

读取操作,不需要同步控制,比较简单

  1. 空tab,直接返回null
  2. 计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null
ublic V get(Object key) {
  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

3.HashTable与ConcurrentHashMap对比

ConcurrentHashMap能完全替代HashTable吗?
​ hash table虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的, hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。

  • Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
  • ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处 是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。

选择哪一个,是在性能与数据一致性之间权衡。ConcurrentHashMap适用于追求性能的场景,大多数线程都只做insert/delete操作,对读取数据的一致性要求较低。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值