java并发编程——ConcurrentHashMap(1.8)

版权声明:坚持原创,坚持深入原理,未经博主允许不得转载!! https://blog.csdn.net/lemon89/article/details/76212739

请先阅读ConcurrentHashMap1.7源码阅读,对JDK1.7(1.6中也基本一致)中的ConcurrentHashMap有个大致了解。

前言

本文通过阅读源码,借助debug方式对费解的地方尝试逐句分析.透彻的讲解1.8版本ConcurrentHashMap

我们阅读源码的思路是从一个使用样例入手,一步步debug去分析:

public class ConcurrentHashMapTest {
    public static void main(String[] args) throws InterruptedException {
        //阅读初始化源码
        Map<String, String> cm = new ConcurrentHashMap<String, String>();

        for (int i = 0; i < 10; i++) {
            //阅读put操作源码
            cm.put("key_" + i, "value_" + i);
        }
        for (int i = 0; i < 12; i++) {
            //阅读get操作源码
            System.out.println(cm.get("key_" + i));
        }
        //others operations ......
    }
}

初始化ConcurrentHashMap

重要成员变量:

    /**
     * 负数(hash buckets正在初始化或者重新扩容):
     *   -1:表示初始化;-|n|表示n-1个线程正在执行resize。
     * 
     * 正数或0:
     *     0:表示hash buckets表还没有被初始化;
     *     正数:初始化hash buckets之前,表示hash buckets的数组size(capacity).
     *     初始化之后,表示下一次扩容的阈值,
     *     它的值始终是当前ConcurrentHashMap容量的0.75倍
     *     (使用位运算提高效率:n - (n >>> 2)==n*0.75),
     *     这与loadfactor是对应的.
     * 
     * 可以看到,与之前版本实现比较,一个volatile int sizeCtl变量充当了多种身份.
     */
    private transient volatile int sizeCtl;


    /**
     * 数组桶.在第一次插入操作(即put)时候完成初始化(lazy init).
     * 思考?这里volatile的意义是什么(volatile修饰一个数组声明):
     * volatitle只能对数组的引用产生作用,而数组内的具体元素则没有
     * volatile语义作用。所以这里只是保证table能指向最新的内存地址。
     * 
     * 由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。
     * 同时每个元素是一个Node实例它的Key值和hash值都由final修饰,
     * 不可变更,无须关心它们
     * 被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障
     */
    transient volatile Node<K, V>[] table;
    /**
     * @param initialCapacity 初始化的容量,通过位运算根据这个值计算出一个2的N次幂的值,来作为 hash buckets数组的size.
     * 默认16
     * @param loadFactor hash buckets的密度,根据这个值来确定是否需要扩容.默认0.75
     * @param concurrencyLevel 并发更新线程的预估数量.默认1.
     */
    public ConcurrentHashMap8(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;//初始化为cap
    }
    /**
     * 根据预期的capacity参数,返回一个2的N次幂
     */
    private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

关于这个talbeSizeFor(int c)所用的算法,详情参考:
tableSizeFor取数算法

ConcurrentHashMap底层数据结构

这里写图片描述
ConcurrentHashMap通过组合一个 Node<K, V>[] table数组+Node单向链表,来作为底层数据储存的结构。

static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;//key的hashcode执行了hash函数后的值

        final K key;

        volatile V val;//volatile保证其可见性,下同

        volatile Node<K, V> next;

        Node(int hash, K key, V val, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        ....

Put

与HashMap不同(允许key、value为null),ConcurrentHashMap中key、value都不允许为null,否则会报NPE。
put

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

putVal

    // onlyIfAbsent默认为false,允许key相同的value被覆盖
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null)
            throw new NullPointerException();
        // hash=(h ^(h >>>16))& HASH_BITS:移位运算使高位参与运算,尽可能分布以便减少哈希冲突
        //这个int hash将作为key对应的结点Node中的成员变量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();//1.数组桶初始化(延迟初始化hash桶,第一次put操作),并计算下一次rehash的阈值
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//i = (n -1) & hash:hash数组桶的index,非常类似hashMap的key计算方法
                //2.如果这个key对应的数组f位置没有元素,则CAS初始化这个f数组元素(单向链表Node对象)
                if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                    break;
            } else if ((fh = f.hash) == MOVED)//3.f结点已经转换为ForwardingNode,表示有其他线程正在扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {//4.锁住链表f(或者红黑树)
                    if (tabAt(tab, i) == f) {//再次判断,如果失败则释放锁
                        if (fh >= 0) {
                            binCount = 1;//记录当前数组桶中的链表Node个数.
                            //5.遍历链表,新增或者覆盖
                            for (Node<K, V> e = f;; ++binCount) {
                                K ek;
                                //5.1:查找是否有重复的key,尝试覆盖(默认覆盖)
                                if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {//当前节点,key.hash&&key匹配
                                    oldVal = e.val;//记录原有value
                                    if (!onlyIfAbsent)//是否允许覆盖 默认允许
                                        e.val = value;//覆盖
                                    break;
                                }
                                //5.2:链表末端上新增一个结点
                                Node<K, V> pred = e;
                                if ((e = e.next) == null) {//移动到下个结点,直到尾部
                                    pred.next = new Node<K, V>(hash, key, value, null);//为null表示到达链表尾部,此时在尾部插入新的结点。否则继续遍历这个链表
                                    break;
                                }
                            }
                        } else if (f instanceof TreeBin) {//6.红黑树则使用红黑树插入
                            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;
                            }
                        }
                    }
                }
                //7.通过binCount判断链表上结点个数,是否需要链表转红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;//当同一个key覆盖value的情况下,直接返回oldVal,无需执行后续计数代码
                    break;
                }
            }
        }
        addCount(1L, binCount);//8
        return null;
    }

接着对put方法中用到的几个方法做进一步解析:
initTable

    /**
     * 1.初始化数组桶
     * 2.确认下次扩容阈值(sizeCtl使用CAS设置)
     */
    private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)//当sizeCtl<0表示当前对象正在初始化,尝试yield cpu时间以避免不必要的竞争
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// CAS设置为-1表示正在初始化
                try {
                    if ((tab = table) == null || tab.length == 0) {//再次判断
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//设置初始数组桶的size.默认数组个数16
                        @SuppressWarnings("unchecked")
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];// 数组桶生成
                        table = tab = nt;
                        sc = n - (n >>> 2);//计算下一次扩容阈值,等价于sc=n*0.75
                    }
                } finally {
                    sizeCtl = sc;// resize 阈值
                }
                break;
            }
        }
        return tab;
    }

addCount

private final void addCount(long x, int check) {//put方法调用:x默认为1;binCount表示链表遍历的当前个数
        CounterCell[] as;
        long b, s;
        if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//当前k\v元素总数,加1
            CounterCell a;
            long v;
            int m;
            boolean uncontended = true;//默认假设不存在竞争
            if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null
                    || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {//下边这段逻辑,会在数量达到阈值时做resize:
            Node<K, V>[] tab, nt;
            int n, sc;
            //当前总数(+1后)>=阈值(sc) && table数组不为null && 数组个数不超标
            while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {//-1:表示初始化;-|n|表示n-1个线程正在执行resize.
                    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))//将sizectl设置为一个很大的负数,然后进行transfer扩容,结束transfer后设置为下一次扩容的阈值
                    transfer(tab, null);//首次执行
                s = sumCount();
            }
        }
    }

待续

猜你喜欢

转载自blog.csdn.net/lemon89/article/details/76212739