面试宝典:数据结构-ConcurrentHashMap

简介: 面试宝典:数据结构-ConcurrentHashMap

image.png


image.png


1、HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁


2、假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术

a、首先将数据分成一段一段的存储


b、然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问


c、有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁


d、在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。


3、ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成


4、Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色


5、HashEntry则用于存储键值对数据


6、一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构


7、一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。


jdk1.8 concurrentHashMap原理分析


1.8版本的ConcurrentHashMap的实现与1.7版本有很大的差别,放弃了段锁的概念,借鉴了HashMap的数据结构:数组+链表+红黑树


  • ConcurrentHashMap不接受nullkey和nullvalue
  • 数据结构:数组+链表+红黑树
  • 并发原理:cas乐观锁+synchronized锁
  • 加锁对象:数组每个位置的头节点


concurrentHashMap数据结构


ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率


image.png


构造函数


1、对于构造函数而言,会根据输入的initialCapacity的大小来确定一个最小的且大于等于initialCapacity大小的2的n次幂,如initialCapacity为15,则sizeCtl为16,若initialCapacity为16,则sizeCtl为16


2、若initialCapacity大小超过了允许的最大值,则sizeCtl为最大值


3、值得注意的是,构造函数中的concurrencyLevel参数已经在JDK1.8中的意义发生了很大的变化,其并不代表所允许的并发数 只是用来确定sizeCtl大小


4、在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。


  • ConcurrentHashMap()

该构造函数用于创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射


  • ConcurrentHashMap(int)

该构造函数用于创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射


  • ConcurrentHashMap(Map<? extends K, ? extends V>)

该构造函数用于构造一个与给定映射具有相同映射关系的新映射


public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        // 将集合m的元素全部放入
        putAll(m);
    }


  • ConcurrentHashMap(int, float)


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




  • ConcurrentHashMap(int, float, int)


putVal函数


V putVal(K key, V value, boolean onlyIfAbsent)


put函数底层调用了putVal进行数据的插入,对于putVal函数的流程大体如下。


① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②


② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③


③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④


④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤

⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥


⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值


初始化table


Node<K,V>[] initTable()


对于table的大小,会根据sizeCtl的值进行设置,如果没有设置szieCtl的值,那么默认生成的table大小为16,否则,会根据sizeCtl的大小设置table大小


获取table指定元素


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


此函数返回table数组中下标为i的结点,可以看到是通过Unsafe对象通过反射获取的,getObjectVolatile的第二项参数为下标为i的偏移地址


比较并交换


static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {
   return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}


此函数用于比较table数组下标为i的结点是否为c,若为c,则用v交换操作。否则,不进行交换操作。


协助转移


Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f)


此函数用于在扩容时将table表中的结点转移到nextTable中


添加节点到红黑树


TreeNode<K,V> putTreeVal(int h, K k, V v)


此函数用于将指定的hash、key、value值添加到红黑树中,若已经添加了,则返回null,否则返回该结点


转换成红黑树


void treeifyBin(Node<K,V>[] tab, int index)


此函数用于将桶中的数据结构转化为红黑树,其中,值得注意的是,当table的长度未达到阈值时,会进行一次扩容操作,该操作会使得触发treeifyBin操作的某个桶中的所有元素进行一次重新分配,这样可以避免某个桶中的结点数量太大。


查询


V get(Object key)


get函数根据key的hash值来计算在哪个桶中,再遍历桶,查找元素,若找到则返回该结点,否则,返回null


删除


replaceNode


此函数对remove函数提供支持,remove函数底层是调用的replaceNode函数实现结点的删除


1、计算hash值

2、进入无限循环 确保删除成功

3、若table表为空或者表长度为0或者key所对应的桶为空 跳出循环

4、若桶中第一个结点的hash值为MOVED 则转移数据

5、删除操作

准备对桶中的第一个节点进行枷锁处理 但看看是否满足前提条件

a、判断桶中的第一个节点是否没有变化

a-1、节点hash值是否大于0

a-2、无限循环链表

a-3、判断 结点的hash值与指定的hash值相等,并且key也相等

a-4、进行替换或删除

b、判断是否为红黑树

b-1、 转换为treeNode结构

b-2、根节点不为空并且存在与指定hash和key相等的结点

b-3、进行替换或删除

相关文章
|
3月前
|
Java
【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
ArrayList是基于可动态扩展的数组实现的,支持快速随机访问,但在插入和删除操作时可能需要数组复制而性能较差。
|
14天前
|
存储 缓存 安全
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
本文详细解析ConcurrentHashMap的实现原理,大厂高频面试,必知必备。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
|
1月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
33 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
4月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
49 0
|
26天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
3月前
|
存储 安全 Java
Java集合类面试十七】、介绍一下ConcurrentHashMap是怎么实现的?
ConcurrentHashMap在JDK 1.7中通过分段锁实现线程安全,在JDK 1.8中则采用Node数组配合链表和红黑树,并使用Synchronized和CAS操作提高并发性能。
Java集合类面试十七】、介绍一下ConcurrentHashMap是怎么实现的?
|
3月前
|
算法 Java
【Java集合类面试十八】、ConcurrentHashMap是怎么分段分组的?
ConcurrentHashMap通过分段锁(Segment)实现高效并发访问,get操作无需加锁,而put操作首先判断是否需要扩容,然后通过两次hash定位并尝试使用CAS和锁机制安全地添加元素。
|
3月前
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。
|
4月前
|
安全 算法 Java
Java面试题:如何使用并发集合,例如ConcurrentHashMap?
Java面试题:如何使用并发集合,例如ConcurrentHashMap?
56 1
|
4月前
|
设计模式 并行计算 安全
Java面试题: 如何使用装饰器模式来增强ConcurrentHashMap的功能?在什么情况下应该使用CopyOnWriteArrayList而不是ArrayList?
Java面试题: 如何使用装饰器模式来增强ConcurrentHashMap的功能?在什么情况下应该使用CopyOnWriteArrayList而不是ArrayList?
32 0
下一篇
无影云桌面