ConcurrentHashMap 源码解析

        今天来看一下ConcurrentHashMap,网上看到很多都是说 它与HashMap 很像,像到什么程度呢?区别仅在于ConcurrentHashMap 在put时加了synchronized 关键字,所以多线程环境下,ConcurrentHashMap是安全的,那么接下来我们来看一下,底层是如何实现的,如果还不了解HashMap可以参考另一篇博文HashMap源码解析

        按照惯例,先来看一下定义的全局变量及含义:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {

    // 最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认数组容量
    private static final int DEFAULT_CAPACITY = 16;

    // 最大数组容量
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 扩容负载因子
    private static final float LOAD_FACTOR = 0.75f;

    // 树化阀值
    static final int TREEIFY_THRESHOLD = 8;

    // 反树化阀值
    static final int UNTREEIFY_THRESHOLD = 6;

    // 最小树化容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 最小转移步幅
    private static final int MIN_TRANSFER_STRIDE = 16;

    private static int RESIZE_STAMP_BITS = 16;

    // 重置最大值
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

}

         从全局变量我们可以看到,里面有一部分变量都是在HashMap中见到过的,并且值是相同的,由此我们可以简单的推断,初始化容量大小、扩容条件以及树化、反树化条件都是一致的,由此可以看出它与HashMap 还是很相似的,下面再来进一步的深入。

一、构造函数

// 无参构造
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;
}

// Map构造(将其他Map转化为ConcurrentHashMap)
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;
}

可以看到,构造函数还有比较多的,能够满足我们的大部分的使用场景。以下分析基于无参构造。 

二、put() 方法

这个方法应该都是非常熟悉了,如果不熟悉只能说明你从没用过Map……

map.put(K key,V value);

看一下底层是如何实现的:

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

        进入到put方法中,发现实际调用的是putVal方法,继续深入(由于该方法代码较多,为了不扰乱我们的视线,先将没有用到的代码屏蔽)

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    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();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // no lock when adding to empty bin
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            ......
        }
    }
    addCount(1L, binCount);
    return null;
}

        由上述代码第二行可以看出,put方法中 key 与 value 均不可为 null 否则回报空指针异常。由第五行代码可知,每次添加新元素时,都会遍历整个map,直到找到可以放置新元素的位置为止。当调用put 第一次进入循环时,table 为null ,所以会调用 initTable 方法 来初始化Map,该方法详细内容见下方,简单来说就是Map的容量设置为默认大小16。第二次循环,table的长度经过初始化之后变成16,但里面每一个桶皆为null,所以会执行到 else if 中,并将元素放到计算的位置,当第一次添加到空桶(也可称空仓 empty bin ,即数组的该位置上没有元素)时不会加锁(源码中也给了很简明的注释)。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

        当我们向map中添加足够多的元素时,hash碰撞是在所难免的,当产生hash碰撞时,在数组的同一个位置形成链表来装载不同元素,此时,为了防止多线程下出现数据出现问题,会在此条件下加锁,源码如下(暂时用不到的代码会屏蔽):

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
           ......
            else {
                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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

        上述代码表示,ConcurrentHashMap 的key 和value 均不可为null,否则直接抛出空指针异常。一旦添加元素时发生hash碰撞,就会找到碰撞的位置的链表,并加锁添加新的元素。直接看最重要的部分也就是最下面的判断,每次进入到锁中都会循环整个数组,知道找到对应的位置,而每一个循环binCount 就是自增1代表元素的个数,一旦达到或者超过默认的树化阀值,就会执行 treeifyBin 方法,即树化函数。

    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (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));
                    }
                }
            }
        }
    }

        进入到 treeifyBin 函数中,在执行真正的树化操作之前还会做一次判断,如果当前元素的个数小于 最小树化容量(64),则尝试扩容操作。否则就会执行树化操作 setTabAt 。

三、get() 方法

        get函数比较简单,因为只是获取map中的内容,而没有对map本身有任何的修改操作,所以只是简单的了解以下,源码如下:

    public 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;
    }

        从上述put函数可知,ConcurrentHashMap 的 key 和 value 均不可为 null ,否则报空指针。那么在 get 函数中 是否允许 key 为 null 呢,从源码中并没有直接判断 key 是否为 null 的代码,但是我们可以从第三行代码看出端倪,要想获取 key 对应的 value ,首先时要获取 key 的 hash 值,那么 key.hashCode() 就是获取 key 的 hash 值,如果 key 为null,那么可想可知 一定会报空指针异常。获取 key 的 hash 值之后就是根据 这个 hash  值 找到对应位置的value 返回,如果没有找到,那么直接返回 null 。

四、remove() 函数

        先来一波源码,可以更容易理解:

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

        当我们执行remove操作的时候,底层实际是调用的 replaceNode ,根据这个函数名称,我们应该也能大概知道 他是怎么删除map中的元素的,先根据函数名猜测replaceNode 实际是根据key 找到对应的位置,并用 null 替换 该位置上的 value。接下来看下源码 是否跟我们猜测的一样:

    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv || (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

        由上述代码看到 首先就是获取 key 的 hash 值,说明 在remove中的key也是不能为null的,删除的细节就不分析了,整体流程和putVal 相差不大,整体思路就是根据key找到对应位置的node 并将 此node 的value设置为null,执行替换操作之后 还会判断此节点是否为树节点的实例,也就是说 当前key所在的结构是否在树结构,再根据相应的条件 进行反树化操作。

本次先读到此处,后续有时间会加上。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值