ConcurrentHashMap源码剖析

       

        ConcurrentHashMap与HashTable类似,都是为了解决并发情况下HashMap的不安全问题,不过ConcurrentHashMap与HashTable比起来效率会更高一些。这里先说明Jdk8之后的ConcurrentHashMap源码。

目录

JDK1.8

成员变量

构造函数

初始化数组

添加/修改

查询

删除


JDK1.8

成员变量

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;
    

    /**
     最大可能的表容量。 该值必须恰好为 1<<30 以保持在 Java 数组分配和两个表大小的幂的索引范围内,并且进一步需要,因为 32 位哈希字段的前两位用于控制目的。
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;


    /**
     默认初始表容量。 必须是 2 的幂(即,至少为 1)且最多为 MAXIMUM_CAPACITY。
     */
    private static final int DEFAULT_CAPACITY = 16;

    
    /**
     可能的最大(非 2 的幂)数组大小。 toArray 和相关方法需要。
     */
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;



    /**
     此表的默认并发级别。 未使用但定义为与此类的先前版本兼容。
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;


    /**
     此表的负载因子。 在构造函数中覆盖此值仅影响初始表容量。 通常不使用实际的浮点值——使用诸如n - (n >>> 2)表达式来表示相关的调整大小阈值会更简单。
     */
    private static final float LOAD_FACTOR = 0.75f;


    /**
     使用树而不是列表的 bin 计数阈值。 将元素添加到至少具有这么多节点的 bin 时,bin 会转换为树。 该值必须大于 2,并且应至少为 8,以与树移除中关于在收缩时转换回普通 bin 的假设相匹配。
     */
    static final int TREEIFY_THRESHOLD = 8;


    /**
    在调整大小操作期间取消(拆分)bin 的 bin 计数阈值。 应小于 TREEIFY_THRESHOLD,最多为 6 以在移除下进行收缩检测。
     */
    static final int UNTREEIFY_THRESHOLD = 6;


    /**
     可以将 bin 树化的最小表容量。 (否则,如果 bin 中的节点过多,则调整表的大小。)该值应至少为 4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间发生冲突。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;


    /**
     每个转移步骤的最小重新组合次数。 范围被细分以允许多个调整器线程。 此值用作下限,以避免调整器遇到过多的内存争用。 该值应至少为 DEFAULT_CAPACITY。
     */
    private static final int MIN_TRANSFER_STRIDE = 16;


    /**
     sizeCtl 中用于生成标记的位数。 对于 32 位数组,必须至少为 6。
     */
    private static int RESIZE_STAMP_BITS = 16;


    /**
     可以帮助调整大小的最大线程数。 必须适合 32 - RESIZE_STAMP_BITS 位。
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;


    /**
     *sizeCtl 中记录大小标记的位移位。
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;


    /*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash


    /** CPUS 数量,用于限制某些尺寸 */
    static final int NCPU = Runtime.getRuntime().availableProcessors();


        static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        ...
    }


    
    /**
     bin 数组。 第一次插入时延迟初始化。 大小始终是 2 的幂。 由迭代器直接访问。
     */
    transient volatile Node<K,V>[] table;


    /**
     * 下一个要使用的表; 仅在调整大小时非空。
     */
    private transient volatile Node<K,V>[] nextTable;


    /**
     基本计数器值,主要在没有争用时使用,但也可作为表初始化竞争期间的后备。 通过 CAS 更新。
     */
    private transient volatile long baseCount;

    /**
     表初始化和调整大小控制。 如果为负,则表正在初始化或调整大小:-1 表示初始化,否则 -(1 + 活动调整大小线程的数量)。 否则,当 table 为 null 时,保存创建时使用的初始表大小,或默认为 0。 初始化后,保存下一个要调整表格大小的元素计数值。
     */
    private transient volatile int sizeCtl;

    /**
     调整大小时要拆分的下一个表索引(加一)。
     */
    private transient volatile int transferIndex;

    /**
     * 调整大小和/或创建 CounterCell 时使用的自旋锁(通过 CAS 锁定)。.
     */
    private transient volatile int cellsBusy;

    /**
     *计数器单元格表。 当非空时,大小是 2 的幂。
     */
    private transient volatile CounterCell[] counterCells;

    // views
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;


}

        并发状态下需要考虑可见性,因此很多成员变量类似Node都用volatile修饰。

这里有个关键的sizeCtl参数:

sizeCtl :默认为0,用来控制table的初始化和扩容操作
-1 代表table正在初始化
-N 表示需要取对应的二进制的低16位数值为M,代表此时有M-1个扩容线程。
其余情况:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍  。

由后面几个成员变量可知里面有了CAS思想。

构造函数


    /**
     * 使用默认的初始表大小 (16) 创建一个新的空映射。
     */
    public ConcurrentHashMap() {
    }

    /**
     创建一个新的空地图,其初始表大小可容纳指定数量的元素,而无需动态调整大小。
    参数:
    initialCapacity – 实现执行内部大小调整以容纳这么多元素。
    抛出:
    IllegalArgumentException – 如果元素的初始容量为负
     */
    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;
    }

    /**
     创建一个与给定地图具有相同映射的新地图。
    参数:
    m——地图
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

    /**
    根据给定的元素数量 ( initialCapacity ) 和初始表密度 ( loadFactor ) 创建一个具有初始表大    小的新的空映射。
    参数:
    initialCapacity – 初始容量。 给定指定的负载因子,实现会执行内部大小调整以容纳这么多元素。
    loadFactor – 用于建立初始表大小的负载因子(表密度)
    抛出:
    IllegalArgumentException – 如果元素的初始容量为负或负载因子为非正
    自从:
    1.6
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    /**
    根据给定的元素数 ( initialCapacity )、表密度 ( loadFactor ) 和并发更新线程数 (     concurrencyLevel ) 创建一个具有初始表大小的新空映射。
    参数:
    initialCapacity – 初始容量。 给定指定的负载因子,实现会执行内部大小调整以容纳这么多元素。
    loadFactor – 用于建立初始表大小的负载因子(表密度)
    concurrencyLevel – 估计的并发更新线程数。 实现可以使用这个值作为调整大小的提示。
    抛出:
    IllegalArgumentException – 如果初始容量为负或负载因子或 concurrencyLevel 为非正
     */
    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;
    }

    // Original (since JDK1.2) Map methods

        我们发现最后一个函数有concurrencyLevel这个参数,这是使用者传入估计的并发线程数,这样初始容量可以根据这个参数来设置。

初始化数组


    /**
     * 使用 sizeCtl 中记录的大小初始化表
     */
    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(); // 让步
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //CAS设置当前线程正在初始化,即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数组
                        table = tab = nt;
                        sc = n - (n >>> 2); //n-n/4=0.75n,初始化完成,0.75倍,实际容量
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

        由此可以看到sizeCtl可以判断当前map的实际状态,是否被其它线程正在初始化,如果是则会通过线程让步取消初始化。这里也印证了sizeCtl当前值的不同状态代表当前map的不同状态。

添加/修改


/**
 将指定的键映射到此表中的指定值。 键和值都不能为空。
可以通过使用等于原始键的键调用get方法来检索该值。
参数:
key – 与指定值关联的键
value – 要与指定键关联的值
返回:
与key关联的前一个值,如果没有key映射,则为null
抛出:
NullPointerException – 如果指定的键或值为 null
*/
public V put(K key, V value) {
    return putVal(key, value, false);
}


    /**
散列 (XOR) 较高位的哈希值降低并强制最高位为 0。由于该表使用二次幂掩码,因此仅在当前掩码之上位有所不同的一组哈希值将始终发生冲突。 (众所周知的例子是在小表中保存连续整数的浮点键集。)所以我们应用一种变换来向下传播较高位的影响。 位扩展的速度、效用和质量之间存在权衡。 因为许多常见的散列集已经合理分布(因此不会从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,所以我们只是以最便宜的方式对一些移位的位进行异或以减少系统损失,以及合并最高位的影响,否则由于表边界而永远不会在索引计算中使用。
     */
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }


    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)//table 为空,需要初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS思想在tab[i]头结点直接插入,成功退出插入操作,失败则说明有其他节点已经插入,继续下一步判断
                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)//table[i]插入失败且,table[i]的hash为-1则说明有其他线程在进行扩容操作,帮助它们一起扩容。
                tab = helpTransfer(tab, f);//里面会调用transfer扩容
            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 || //找到相同key值的元素,更新
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) //key并不是不存在时才有效
                                        e.val = value; //更新值
                                    break;
                                }
                              	
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {//不同的key值,需要往该node后加一个node
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { //如果待插入位置的元素是树(TreeBin)
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) { //返回null表示插入成功,否则表示找到一个已存在的节点
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD) //如果链表长度大于8,将链表转为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }


//Unsafe 类中  volatile
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    //未加锁时CAS思想插入
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) { //cas思想在i位置处插入node
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }




    /**
添加计数,如果表太小且尚未调整大小,则启动传输。 如果已经调整大小,则在工作可用时帮助执行转移。 传输后重新检查占用情况,以查看是否已经需要再次调整大小,因为调整大小是滞后添加的。
参数:
x - 要添加的计数
检查 - 如果 <0,不检查调整大小,如果 <= 1 只检查是否无竞争
     */
    private final void addCount(long x, int check) {
    ...//一些cas操作  里面也涉及到transfer
    
    }

        由此看出,就添加/修改操作,在jdk1.8中 ConcurrentHashMap就使用了 CAS+synchronized+红黑树 进行了实现。

  • CAS:插入时第一次找到数组索引位置时发现为空(null),利用cas思想插入该节点。
  • synchronized:在该数组索引位置发生hash碰撞冲突时,锁住该位置头结点。
  • 红黑树:与jdk1.8后的hashMap一样,链表长度过长时会转化为红黑树。

查询


    /**
    返回指定键映射到的值,如果此映射不包含键的映射,则返回null 。
    更正式地说,如果此映射包含从键k到值v的映射,使得key.equals(k) ,则此方法返回v ; 否则返回null 。 (最多可以有一个这样的映射。)
    抛出:
    NullPointerException – 如果指定的键为空
     */
    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) //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //查找,查找到就返回

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



        /**
         * 对 map.get() 的虚拟化支持; 在子类中被覆盖。
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }

        可以看出如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回 。

删除


    /**
    从此映射中删除键(及其相应的值)。 如果键不在地图中,则此方法不执行任何操作。
    参数:
    key – 需要删除的键
    返回:
    与key关联的前一个值,如果没有key映射,则为null
    抛出:
    NullPointerException – 如果指定的键为空
     */
    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

    /**
    四个公共删除/替换方法的实现: 用 v 替换节点值,条件是 cv 匹配(如果非空)。 如果结果值为空,则删除。
     */
    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;
    }

删除操作也需要锁住对应位置第一个节点。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值