源码阅读系列——基础篇(ConcurrentHashMap 集合源码分析)

上篇我们讲到HashMap,从整个代码的实现上,我们看到没有任何一个关于synchronized或者Lock的字眼,所以HashMap是线程不安全的,Java提供了一个线程安全的HashMap,当然也可以通过Collections.synchronizedMap来实现Map线程安全,与List里面提到的方式一样,也是很暴力的直接给每个读取方法加一个synchronized字段,这里不再赘述。我们讲一下Java的线程安全利器ConcurrentHashMap

1、类定义

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
        transient volatile Node<K,V>[] table;
    private transient volatile Node<K,V>[] nextTable;
    private transient volatile long baseCount;
    private transient volatile int sizeCtl;
    private transient volatile int transferIndex;
    private transient volatile int cellsBusy;
    private transient volatile CounterCell[] counterCells;
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;

    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));
        this.sizeCtl = cap;
    }
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }
}

2、增加元素

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

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        // ConcurrentHashMap 不允许key和value为null,这个hashmap是没有限制的,注意
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode()); // 计算hash值,见注1
        int binCount = 0;
        // 这里是一个死循环,所以一定要内部break了,才能出来
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); // 如果table未生成,先初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {   // 如果对应hash值位置没有元素,则在相应位置生成一个node节点,作为链表和树的根节点,至于tabAt与casTabAt不是java层实现的,实际就是CAS的方式实现,具体见3节,而且这边的操作也是不加锁的。
                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)
                // 正在扩容中,则先扩容,下一次for循环进来,在添加,见注2
                tab = helpTransfer(tab, f);
            else {
                // 其他情况,则进行hashmap中提到的链表插入或者红黑树插入
                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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

注1:
spread函数

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

从hashmap计算hash余数的时候,我们怎么计算的?
hash * (capacity - 1) ,这样会带来什么问题,在capacity较小的时候,我们用到的更多的是hash值的低位信息,所以为了充分利用hash值的特征,将hash值的低16位和高16位进行异或计算,这样目的是为了减少碰撞。

注2:
helpTransfer函数

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果当前table的的元素正在扩容,则
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length); // 算出当前table长度的扩容标识
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {  // sizeCtl为负数时标识正在扩容
             // 判断当前是否需要新建扩容线程
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs // 是不是与当前的扩容标识相同
            || sc == rs + 1 // 当前是不是扩容完成,因为每次新起扩容线程时,会+1,而完成一个扩容,又会-1
            || sc == rs + MAX_RESIZERS // 当前扩容线程是否达到最大
            || transferIndex <= 0)
                break;
            // 新建一个扩容线程,此时设置sizeCtl + 1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab); // 扩容具体操作,见章节4
                break;
            }
        }
        return nextTab;
    }
    return table;
}

此函数,如名字,叫帮助扩容,实际上就是帮助当前已有的扩容行为B创建扩容线程T。本身扩容行为B就是由扩容线程T组成,所以创建线程时,需要校验有扩容行为B。那rs = resizeStamp(tab.length)实际上就是这个标识,这个标识与当前的table长度一一对应,所以可以作为标识。而为了能让sizeCtl同时表示标识和当前线程数量,则首先将rs左移16位,也就是高位,而低16位则表示线程数量,所以才可以保证+1表示线程增加,-1表示线程完成。

3、CAS的原理
全称是比较和交换(Compare And Swap),这个 被广泛用在jdk8的很多多线程机制里,实际上是jvm通过c代码实现的,因为底层是通过cpu来实现的,所以不同处理器的方式有所不同。大致原理如下:
每个线程需要改变值的时候,首先会拷贝一份该值的复制,比如原来是1,那么当某一个线程需要改变值的时候,他首先会把自己工作内存的值与主内存中值,进行compare,如果发现值变更了,则认为此操作失败,重新进行操作,知道发现compare的值相同时,进行swap操作,更改主内存中的值。所以这种方式可以在大多数情况下,实现原子操作,但是也有例外,因为他比较的只是值,所以一旦出现ABA,比如一个值从A变成了B又从B变成了A,但是对于某一个线程,他去执行compare的时候,就不认为该值变化了。所以java提供了了一种AtomicStampedReference增加一个版本号进行变更,可以避免此问题。

4、扩容理解

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 根据CPU数量,计算以几个一组(桶区间)进行计算
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // 默认是16个作为一组
        // 如果nextTab为空,也就是默认扩容的时候,先初始化新的table
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 新的table扩容两倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;  // 迁移前的table长度
        }
        int nextn = nextTab.length;
        // 被标识为ForwardingNode的桶,认为被一个线程占用
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true; // 当前的线程是否处理完成
        boolean finishing = false; // 用于标识是否扩容完成
        // bound 表示当前线程可以处理的当前桶区间最小下标
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) { // 4.1循环
                int nextIndex, nextBound;
                // 4.2默认i是大于bound的,首次基本会进入到下面4.4的逻辑,然后每处理完一个元素(桶)之后,就会再次走入4.1的循环,进行-1操作之后,如果这一组还没有处理好,就会一直满足4.2的条件,此时就会直接跳出4.1循环,进行元素的处理,一旦不满足4.2了,也就是处理完桶区间的最小下标元素了,就会走到4.3(完成扩容线程的分配)或者4.4 处理下一组的条件里
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) { // 4.3当前最大下标已经是table的长度了。表示已经完成线程分配了
                    i = -1;
                    advance = false;
                }
                // 4.4设置当前线程要处理的下一组bound
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1; // nextIndex - 1是当前处理区间的最大下标
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 完成扩容了
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 表示当前已经扩容完成,这个在addCount函数中有涉及(想了解的具体可参看源码),首次创建线程的时候,创建的首个线程的标识,就是通过计算 resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2得到,因为增加线程是+1,完成一个线程扩容是-1,所以当所有的都完成了,就恢复到了resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2。
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);  // 如果原空间是null,则直接将该位置设置为ForwardingNode,也就是标识一下该元素被占用。
            else if ((fh = f.hash) == MOVED)
                advance = true; // 表示已经完成了扩容
            else {
                // 锁元素(桶),也就是在真正进行扩容操作的时候,才对元素加锁。而不是暴力的对整个hashmap对象上锁,效率很高。
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            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);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            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;
                                }
                            }
                            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);
                            advance = true;
                        }
                    }
                }
            }
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMapJava 并发包中的一个线程安全的哈希表实现。它采用了分段锁(Segment)的机制来提供高并发性能。下面是简要的 ConcurrentHashMap源码分析ConcurrentHashMap 的整体结构是由多个 Segment 组成的,每个 Segment 内部都是一个 HashEntry 数组,每个数组元素都是一个链表的头节点。每个 Segment 都维护着自己的锁,这样不同的线程可以同操作不同的 Segment。 在 ConcurrentHashMap 中,关键方法 put、get、remove 等都是通过计算键的哈希值得到对应的 Segment,然后进行对应的操作。这样多个线程可以并行地对不同的 Segment 进行操作,从而提高了并发性能。 ConcurrentHashMap 的 put 方法首先根据 key 的哈希值定位到对应的 Segment,然后使用锁来保证线程安全。如果键已经存在,则会替换对应的值;如果键不存在,则会创建新的节点并添加到链表中。 ConcurrentHashMap 的 get 方法也是根据 key 的哈希值定位到对应的 Segment,然后通过遍历链表来找到对应的节点,并返回节点中的值。 ConcurrentHashMap 的 remove 方法同样也是根据 key 的哈希值定位到对应的 Segment,然后通过遍历链表来找到对应的节点,并将节点从链表中移除。 需要注意的是,在进行扩容操作ConcurrentHashMap 会创建新的 Segment 数组,并将每个 Segment 中的元素重新散列到新的数组中。 总之,ConcurrentHashMap 通过使用分段锁的方式来提供高并发性能,同保证线程安全。每个 Segment 内部是一个独立的哈希表,对不同的 Segment 可以进行并发操作。这使得 ConcurrentHashMap 成为了高效的并发哈希表实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值