ConcurrentHashMap源码实现分析

为什么要使用ConcurrentHashMap

在多线程环境下,使用HashMap进行put操作,会引起死循环,导致CPU利用率接近100%。HashMap在并发执行put操作时会形成一种环形链表,一旦形成环形数据结构,Entry的nex节点不会为空,当查找一个链表中不存在的数据时,会引起死循环。

原因

在多并发情况家,一个线程先期完成了扩容,将原来的列表散列到自己的表中,形成了一个倒序,而另一个线程进行扩容时,将已倒序的表又散列成为正序,这样会产生一个环形,当get一个不存在的节点时,会产生一个死循环。所以在多线程环境下应该使用ConcurrentHashMap。

ConcurrentHashMap实现分析

在这里插入图片描述
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。Segment结构与HashEntry类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表元素。每个Segment元素守护HashEntry里边的元素。

构造方法初始化

    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
        this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
    }

ConcurrentHashMap是通过initialCapacity,loadFactor和concurrencyLevel进行初始化,他们分别表示装载容量、负载因子和并发数量。其中concurrencyLevel 默认是DEFAULT_CONCURRENCY_LEVEL = 16;即容器里边分段锁的个数,也可以理解为默认并发度为16。segment的长度是通过concurrentyLevel计算得出的,segment的长度为2的N次方,所以必须计算出一个大于等于concurrentcyLevel的最小2的N次方来做Segment的长度。加入currentcylevel等于11或者12,则ssize等于12,即容器锁的个数也为12。

ConcurrentHashMap使用Segement分段锁来保护不同断的数据,在插入和获取元素之后都需要散列算法进行定位。ConcurrentHashMap会先使用Wang/Jenkins 变种算法进行散列。

get操作先使用hash算法定位Segment位置(使用了散列值的高位部分),再通过散列出的值再散列出值,定位HashEntry。

    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        //定位锁
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
       //拿到到segment下的table
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            //开始遍历table下指定的Entry值
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

get方法没有加锁,其原因是HashEntry的value和next使用的是volatile修饰。保证每次取到的值都是最新值。

 transient volatile HashEntry<K,V>[] table;
put操作

在这里插入图片描述
segment[0]在map初始化的时候已经被初始化。

      // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;

ensureSegment函数,循环遍历,当有一个线程操作成功便跳出循环。

 while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }

put会通过tryLock获得锁,如果成功获得所返回null,否则通过scanAndLockForPut自旋的方式获得锁,

            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);

scanAndLockForPut函数,通过自旋达到一定的次数还未获得锁,便调用locak阻塞式拿锁,在此过程中还不忘利用cpu的资源,创建一个node节点。

        while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;

获得锁之后采用头插法,将元素插入到链表的head位置。

                      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
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;

rehash 操作

扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性table。

remove 操作

与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值