Java基础之ConcurrentHashMap原理分析

ConcurrentHashMap

HashMap不是线程安全的,而当使用同步集合Collections.synchronizedXXX方法来实现HashMap同步的话,因为它同步的粒度比较大。也就是每次加锁的时候,都会对整个对象加锁。从而导致在并发的场景下,效率比较低。

而ConcurrentHashMap的好处就在于,每次插入元素的时候都只对整个集合的一部分进行加锁。这样在并发的情景之下,性能就比HashMap的同步集合效率高。

那么,它是如何实现的呢?

在JDK1.8之前使用的是锁分段的技术,但是在1.8之后锁分段实现方式有了变化,不再是之前使用Segment来加锁了,而是对数组的某个元素进行加锁,而且里面使用了CAS的理念。

比如,所有的put方法,实际上是调用了下面的方法的实现:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    // binCount是该桶中的元素的数量,默认是0
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 当table的为null,或者列表长度为空,就对table进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 然后根据key的哈希来获取指定的table数组的元素(即一个链表或红黑树)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 这里就是基于CAS的理念,向table中插入结点,casTabAt方法参考下面
            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 (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // 这里设置binCount为1,然后每次插入元素的时候binCount+1
                        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;
                        }
                    }
                }
            }
            // 这里是当指定的桶的长度大于8的时候,调整桶的数据结构
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 
    addCount(1L, binCount);
    return null;
}

这里就是上面的插入操作,这里的U就是sun.misc.Unsafe的实例。这里调用了sun.misc.Unsafe的compareAndSwapObject方法,它是一个native方法。可以这样理解它:它判断参数tab在内存中偏移((long)i << ASHIFT) + ABASE处的元素是否为null,如果为c,就将v插入到指定的位置c中。

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方法

Size方法的目的是统计ConcurrentHashMap的总元素数量, 自然需要把各个Segment内部的元素数量汇总起来。

但是,如果在统计Segment元素数量的过程中,已统计过的Segment瞬间插入新的元素,这时候该怎么办呢?

ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:

  1. 遍历所有的Segment。
  2. 把Segment的元素数量累加起来。
  3. 把Segment的修改次数累加起来。
  4. 判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
  5. 如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
  6. 再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
  7. 释放锁,统计结束。

为什么这样设计呢?这种思想和乐观锁悲观锁的思想如出一辙。

扫描二维码关注公众号,回复: 804188 查看本文章

为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。

public int size() {
   final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // size超过32 bits则为true
    long sum;         // 修改的总次数modCounts
    long last = 0L;   // 之前的sum
    int retries = -1; // 第一次遍历时不进行重试
    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // 加锁
            }
            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 {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

参考:http://mp.weixin.qq.com/s?__biz=MzI1MTIzMzI2MA==&mid=2650561829&idx=1&sn=ba98a1f5c312ba19ad81abf59feadae0&chksm=f1feeba6c68962b00ff675088afa7e068a4b1c1e61d0abde3d02e0781050f0d8e8305122e9e8&scene=0#rd

猜你喜欢

转载自blog.csdn.net/github_35186068/article/details/78695188
今日推荐