全国旗舰校区

不同学习城市 同样授课品质

北京

深圳

上海

广州

郑州

大连

武汉

成都

西安

杭州

青岛

重庆

长沙

哈尔滨

南京

太原

沈阳

合肥

贵阳

济南

下一个校区
就在你家门口
+
当前位置:首页  >  技术干货  >  详情

ConcurrentHashMap弱一致的迭代器是什么原理?

来源:千锋教育
发布人:xqq
2023-10-14

推荐

在线提问>>

一、ConcurrentHashMap弱一致的迭代器是什么原理

ConcurrentHashMap 的弱一致性迭代器是基于快照迭代器实现的。在 ConcurrentHashMap 中,每个 Segment(即 ConcurrentHashMap 中每个 key-value 对所在的 segment)维护了一个元素计数器 modCount 和一个修改次数计数器 count。当 segment 发生修改操作时,count 计数器会自增,同时 modCount 计数器也会自增。当使用迭代器对 ConcurrentHashMap 进行遍历时,迭代器会在每次访问之前,记录当前 modCount 值,若在迭代过程中 ConcurrentHashMap 发生了修改操作,modCount 值会发生变化,迭代器会发现这个变化,会抛出 ConcurrentModificationException 异常。

因此,ConcurrentHashMap 迭代器的弱一致性体现在它不保证遍历结果即使在迭代过程中 ConcurrentHashMap 发生了修改,但它保证迭代器不会因为在遍历时 ConcurrentHashMap 包含了新元素而抛出异常。同时这种设计也能保证在大多数情况下,迭代器访问的元素都不会太过陈旧,可以满足迭代器的绝大部分使用场景。

二、ConcurrentHashMap简介

java.util.concurrent.ConcurrentHashMap 属于 JUC 包下的一个集合类,可以实现线程安全。它由多个 Segment 组合而成。Segment 本身就相当于一个 HashMap 对象。同 HashMap 一样,Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

ConcurrentHashMap的put方法流程(JDK1.7):

   public V put(K key, V value) {       //key,value不能为null        Segment s;        if (value == null)            throw new NullPointerException();       //通过key进行哈希到对应segment位置        int hash = hash(key);        int j = (hash >>> segmentShift) & segmentMask;        //通过位置j获取当前的对应segment起始位置        if ((s = (Segment)UNSAFe.getObject          // nonvolatile; recheck             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment            s = ensureSegment(j);        return s.put(key, hash, value, false);    }  #内部类Segment下的put方法        final V put(K key, int hash, V value, boolean onlyIfAbsent) {            //尝试性加锁            HashEntry node = tryLock() ? null :                scanAndLockForPut(key, hash, value);            V oldValue;            try {                //当前segment下的table                HashEntry[] tab = table;                //通过key的哈希值进行哈希找到对应table位置                int index = (tab.length - 1) & hash;                HashEntry first = entryAt(tab, index);                for (HashEntry e = first;;) {                    if (e != null) {                        K k;                        if ((k = e.key) == key ||(e.hash == hash && key.equals(k))) {                            oldValue = e.value;                            if (!onlyIfAbsent) {                            //put方法处理:将新value替换oldvalue                                e.value = value;                                ++modCount;                            }                            break;                        }                        e = e.next;                    } else {                        if (node != null)                            node.setNext(first);                        else                            node = new HashEntry(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;                    }                }            } finally {            //释放锁                unlock();            }            return oldValue;        }             //扩容仅针对某个segment进行扩容,而不是对整个ConcurrentHashMap进行扩容      private void rehash(HashEntry node) {          //在segment下的table            HashEntry[] oldTable = table;            int oldCapacity = oldTable.length;            //按照原大小2倍关系进行扩容             int newCapacity = oldCapacity << 1;            threshold = (int)(newCapacity * loadFactor);            HashEntry[] newTable =(HashEntry[]) new HashEntry[newCapacity];            int sizeMask = newCapacity - 1;            //将原有table上的所有hashentry节点进行重新哈希到新table上            for (int i = 0; i < oldCapacity ; i++) {                HashEntry e = oldTable[i];                if (e != null) {                    HashEntry 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 lastRun = e;                        int lastIdx = idx;                        for (HashEntry last = next;                             last != null;                             last = last.next) {                            int k = last.hash & sizeMask;                            if (k != lastIdx) {                                lastIdx = k;                                lastRun = last;                            }                        }                        newTable[lastIdx] = lastRun;                        // Clone remaining nodes                        for (HashEntry p = e; p != lastRun; p = p.next) {                            V v = p.value;                            int h = p.hash;                            int k = h & sizeMask;                            HashEntry n = newTable[k];                            newTable[k] = new HashEntry(h, p.key, v, n);                        }                    }                }            }            int nodeIndex = node.hash & sizeMask; // add the new node            node.setNext(newTable[nodeIndex]);            newTable[nodeIndex] = node;            table = newTable;        }

三、ConcurrentHashMap和HashMap的区别

ConcurrentHashMap和HashMap的区别如下:

HashMap是非线程安全的;而ConcurrentHashMap是线程安全的。HashMap的key和value均可以为null;而ConcurrentHashMap的key和value均不可以为null。HashMap是通过给整张散列表加锁的方式来保证线程安全,这种方式保证了线程安全,但是并发执行效率低下;ConcurrentHashMap在JDK1.8之前,采用分段锁机制来保证线程安全的,这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,多线程就是完全并发的,并发执行效率会提高)。但从JDK1.8开始,ConcurrentHashMap数据结构与1.8中的HashMap保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchronized来保证线程安全的。当多线程并发向同一个散列桶添加元素时。若散列桶为空,此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。

延伸阅读1:JDK1.7 与 JDK1.8 中 ConcurrentHashMap 的区别

数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。保证线程安全机制:JDK1.7 采用Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8采用CAS+synchronized保证线程安全。锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8调整为对每个数组元素加锁(Node)。链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。查询时间复杂度:从JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

相关文章

Kotlin对APP测试意味着什么?

为什么Java后端开发没有大规模采用 Kotlin?

Python有哪些常用的标准库?

哪些技术会决定前端开发者的未来发展?

主流图片加载库所使用的预解码究竟干了什么?

开班信息 更多>>

课程名称
全部学科
咨询

HTML5大前端

Java分布式开发

Python数据分析

Linux运维+云计算

全栈软件测试

大数据+数据智能

智能物联网+嵌入式

网络安全

全链路UI/UE设计

Unity游戏开发

新媒体短视频直播电商

影视剪辑包装

游戏原画

    在线咨询 免费试学 教程领取