sheng的学习笔记-ConcurrentHashMap(JDK1.7和16)源码分析

1.7版本

概述

注意,以下代码都是1.7版本(不同版本代码不一样),最下面有1.8版本部分内容

ConcurrentHashMap是线程安全的key value存储结构,底层也是数组+链表的结构

下面是构造图,存放数据的是Segments数组(元素是Segment类),每个Segment类中都有HashEntry的数组,每个 HashEntry 是一个链表结构的元素

注意,用 volatile修饰table数组,一个线程修改后别的线程立刻可见

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        // 快速失败(fail—fast)用到的变量
    transient int modCount;
        // 大小
    transient int threshold;
        // 负载因子
    final float loadFactor;

}

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock,

理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

每个 Segment 维护着一个 HashEntry 数组里的元素,当要对 HashEntry 的数据进行修改时,就必须先获得对应的 Segement 锁

为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使concurrentHashmap。

JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.

初始化

 下面是this的源码和对应的注释

    /**
     * Creates a new, empty map with the specified initial
     * capacity, load factor and concurrency level.
     *
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements.
     * @param loadFactor  the load factor threshold, used to control resizing.
     * Resizing may be performed when the average number of elements per
     * bin exceeds this threshold.
     * @param concurrencyLevel the estimated number of concurrently
     * updating threads. The implementation performs internal sizing
     * to try to accommodate this many threads.
     * @throws IllegalArgumentException if the initial capacity is
     * negative or the load factor or concurrencyLevel are
     * nonpositive.
     */
    @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;   //计算Segment数组的长度
//不停乘以2,找到大于并发要求值的最小2的倍数,用于当成segment的数组长度
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
//初始化要求的长度除以segment数组长度,
//是为了计算每个segment中的数组HashEntry数组应该有多少个元素,
        int c = initialCapacity / ssize;
//c的值乘以ssize必须大于initialCapacity,因为c是除的结果,
//如果有余数,c+1再乘ssize肯定大于initialCapacity
        if (c * ssize < initialCapacity)
            ++c;
//用于计算c的值是否小于最小值,如果c很小(等于1),
//默认segment有2个元素,如果c的值大,找到大于c的2的倍数
//cap用于构造一个segment元素中数组的长度
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
//为了减少占用空间,除了第一个 Segment 之外,
//剩余的 Segment 采用的是延迟初始化的机制,
//仅在第一次需要时才会创建(通过 ensureSegment 实现)
//方便后面创建的segment对象可以直接使用这个segment的参数,不需要重复计算
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,javascript:void(0)V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
//为了保证延迟初始化存在的可见性,
//访问 segments 数组及 table 数组中的元素均通过 volatile 访问,
//主要借助于 Unsafe 中原子操作 getObjectVolatile 来实现
// 将s0插入到下标index=0上
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

新增元素

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p> The value can be retrieved by calling the <tt>get</tt> method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>
     * @throws NullPointerException if the specified key or value is null
     */
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        //通过key计算hash值
        int hash = hash(key);
        //通过hash计算出segment数组下标
        int j = (hash >>> segmentShift) & segmentMask;
        //如果数组下标的元素为空,生成segment元素并赋值给s
        //如果数组下标的元素不为空,就赋值给s,获取元素是CAS操作
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        //在数组下标的元素中新增值
        return s.put(key, hash, value, false);
    }

此处跟HashMap很像,

hash的计算

  • 如果是字符串,用字符串的hash进行替换,减少hash冲突的概率
  • 获取key的hash code,然后跟h做异或(相同为0,不同为1)
  • 最下面那几行,目的是在散列计算中减少碰撞的概率,但为啥那么写,我也没看懂
     

ensureSegment返回一个segment

    /**
     * Returns the segment for the given index, creating it and
     * recording in segment table (via CAS) if not already present.
     *
     * @param k the index
     * @return the segment
     */
    @SuppressWarnings("unchecked")
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        //计算索引下标
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        //如果索引下标元素为空值,生成一个元素对象
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //用第0个元素的值构造新的segment元素,避免计算,速度快
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            //生成新的元素中HashEntry列表
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            //再次看下元素是否为空,每次都是CAS原子操作,但是防止不了多线程并发
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                //生成新的segment元素
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                //如果获取到的元素为空,用CAS替换,
                //如果替换没成功再次while查看,如果被其他线程并发替换了,while就退出
                //自旋锁的设计,不停while循环尝试
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

segment插入元素

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //获取可重入锁,如果没有获得锁就一直等着
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //找到segment对应的table
                HashEntry<K,V>[] tab = table;
                //根据hash算出下标
                int index = (tab.length - 1) & hash;
                //找到HashTable列表的首节点,要遍历链表
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    //从首节点开始遍历,查看是否有KEY相同的元素,有就覆盖
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        //遍历到链表的尾部,e为null,要插入新的元素到链表中
                        //用头插法,新增节点node的next指向first,然后将node放到头结点
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            //扩容逻辑
                            rehash(node);
                        else
                            //将新增节点放到HashEntry数组的下标元素(链表首个元素)中
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //最终解锁
                unlock();
            }
            return oldValue;
        }

scanAndLockForPut() 

        /**
         * Scans for a node containing given key while trying to
         * acquire lock, creating and returning one if not found. Upon
         * return, guarantees that lock is held. UNlike in most
         * methods, calls to method equals are not screened: Since
         * traversal speed doesn't matter, we might as well help warm
         * up the associated code and accesses as well.
         *
         * @return a new node if key not found, else null
         */
        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            //获取在Segment对应位置的首节点
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            //找到标志位就会变为0
            int retries = -1; // negative while locating node 
            //不停的做自旋锁,尝试抢锁 
            while (!tryLock()) {// 尝试加锁,无论是否抢到锁,都立刻返回值,不阻塞
                // 循环遍历e,直到找到key值相等或为null
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node
                            //如果首节点是空的,new个新节点
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key)) //找到KEY一样的节点
                        retries = 0;
                    else
                        e = e.next;//首节点不是空的而且KEY不一样,在链表中往后遍历
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    //while超过最大重试次数,用阻塞的锁,只有等到抢到锁了才会往下走
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    // 在抢锁的过程中,有可能别的线程修改了值
                    // retries为偶数,且重现获取的首节点值与first不相等时
                    // 重新进行遍历
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

entryForHash()

 static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {
     HashEntry<K,V>[] tab;
     // 根据hash值获取对应在Segment中的值
     return (seg == null || (tab = seg.table) == null) ? null :
         (HashEntry<K,V>) UNSAFE.getObjectVolatile
         (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
 }

扩容rehash()

        /**
         * Doubles size of table and repacks entries, also adding the
         * given node to new table
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K,V> node) {
            /*
             * Reclassify nodes in each list to new table.  Because we
             * are using power-of-two expansion, the elements from
             * each bin must either stay at same index, or move with a
             * power of two offset. We eliminate unnecessary node
             * creation by catching cases where old nodes can be
             * reused because their next fields won't change.
             * Statistically, at the default threshold, only about
             * one-sixth of them need cloning when a table
             * doubles. The nodes they replace will be garbage
             * collectable as soon as they are no longer referenced by
             * any reader thread that may be in the midst of
             * concurrently traversing table. Entry accesses use plain
             * array indexing because they are followed by volatile
             * table write.
             */
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;//新扩容长度为老长度2倍
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity]; //申请新的数组
            int sizeMask = newCapacity - 1;
            //数组中每个元素都是个链表,以元素为维度for循环,每个元素还要处理链表
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                //如果元素为null,不用做操作,如果不为null,要挪到新的数组上
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;
                    //如果单节点,链表中就一个节点,只挪一个节点就可以了
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        //链表上有多个节点,整个链表节点都要挪到新的数组上
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //看看整个链表在新数组的位置是不是都和头节点一样
                        //如果新的索引下标有和头结点不一样的节点
                        //(老的数组一样下标的元素在新的元素中可能下标不一致)
                        //找到第一个和头节点不一致的元素,比如1~5节点在老的数组中都是0下标
                        //但新的数组可能:1~3在0下标,4~5在8下标(只是举个例子)
                        //此时找到新的数组中,和头结点(1节点)不一样的第一个下标(4节点)
                        //找到4节点后5节点就不会修改变量,因为lastIdx已经修改过了
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //将上述例子的4节点放到新的数组中
                        newTable[lastIdx] = lastRun;
                        //将上述1~3节点挪到新数组中,头插法
                        // Clone remaining nodes
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //将新增加的节点放到列表中,上面的代码只是扩容,还没插入新节点
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

获取元素

计算在哪个segment中,通过CAS查到内存数据,在segment元素中查到在HashEntry数组中哪个节点,然后根据节点遍历链表,key值地址相等或者(hash 和equasl同时相等)查找元素

注意这地方没有锁,由于是volatile作用,保持可见性,直接查内存,所以不用担心查到脏数据

16版本

concurrenthashmap1.7和1.8的区别

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表。 从1.7到1.8版本,由于HashEntry从链表 变成了红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n))。

HashEntry最小的容量为2,Segment的初始化容量是16,HashEntry在1.8中称为Node,链表转红黑树的值是8 ,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构。

其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,

  • JDK1.7版本的ReentrantLock+Segment+HashEntry,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
  • JDK1.8版本中synchronized+CAS+HashEntry+红黑树,移除Segment,使锁的粒度更小,Synchronized + CAS + Node

概述

数据存放就一个数组table,没有segment了。

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

初始化

此处并没有申请数组空间,是在PUT的时候做数组空间初始化

新增元素

put

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // put的ket、value不能为null
        if (key == null || value == null) throw new NullPointerException();
        //通过code计算hash,使用扰动处理,减少碰撞概率
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            //如果第一次调用put,初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果hash的位置还没有值,CAS新增一个元素就返回
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            //以下逻辑都是在HASH位置上已经有值了,走链表逻辑
            //扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //onlyIfAbsent一般是false,不会走到这个逻辑
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            else {
                V oldVal = null;
                //HASH对应数组元素头结点加锁
                synchronized (f) {
                    //重新计算数组HASH位置头结点有没有被篡改,防止高并发
                    if (tabAt(tab, i) == f) {
                        //fh是hash,大于0走链表,否则走红黑树
                        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)))) {
                                    //这是找到相同的KEY,替换VALUE
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //不停寻找链表的next节点,直到末尾还没找到就新增一个节点
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key, value);
                                    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;
    }

initTable()

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) { // 若当前table未初始化,进入循环
        if ((sc = sizeCtl) < 0) // sizeCtl<0 代表在初始化或调整大小
            Thread.yield(); // 让出cpu调度时间
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS 将sizeCtl修改为-1,表示当前table正在初始化
            try {
                if ((tab = table) == null || tab.length == 0) { // 双重锁检查
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // sc大于0表示,初始化了容量直接使用该容量,反之则使用默认容量16
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt; // 初始化table
                    sc = n - (n >>> 2); // sc保存当前扩容阈值,即为0.75x当前容量
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

获取数据

//会发现源码中没有一处加了锁
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //计算hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
        if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

总结: 

  • 在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
  • get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
  • 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。

 参考文章:

百度安全验证

ConcurrentHashMap1.8源码解析_坚持一定很Cool的博客-CSDN博客

ConcurrentHashMap1.7源码解析_坚持一定很Cool的博客-CSDN博客_concurrenthashmap源码分析1.7

ConcurrentHashmap(1.8)get操作——为什么它不需要加锁呢_刘翔UP的博客-CSDN博客_concurrenthashmapget为什么不加锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值