Java集合详解10:ConcurrentHashmap面试题汇总,最近找工作老是被问!
Posted 黄小斜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合详解10:ConcurrentHashmap面试题汇总,最近找工作老是被问!相关的知识,希望对你有一定的参考价值。
ConcurrentHashMap篇
HashMap 和 ConcurrentHashMap 的区别
- ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
- HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
- 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
- Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
JDK1.8
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
结构如下:
附加源码,有需要的可以看看
插入元素过程(建议去看看源码):
如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;
-
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
-
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
-
break; // no lock when adding to empty bin
-
}
如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的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)))) {
-
oldVal = e.val;
-
if (!onlyIfAbsent)
-
e.val = value;
-
break;
-
}
-
Node<K,V> pred = e;
-
if ((e = e.next) == null) {
-
pred.next = new Node<K,V>(hash, key, value, null);
-
break;
-
}
-
}
-
}
- 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
- 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;
15.Java中的另一个线程安全的与HashMap极其类似的类是什么?同样是线程安全,它与HashTable在线程同步上有什么不同?
ConcurrentHashMap
类(是Java
并发包java.util.concurrent
中提供的一个线程安全且高效的HashMap
实现)。HashTable
是使用synchronize
关键字加锁的原理(就是对对象加锁);
而针对ConcurrentHashMap
,在JDK1.7
中采用分段锁的方式;JDK1.8
中直接采用了CAS
(无锁算法)+synchronized
。
16.HashMap&ConcurrentHashMap的区别?
除了加锁,原理上无太大区别。另外,HashMap
的键值对允许有null
,但是ConCurrentHashMap
都不允许。
17.为什么ConcurrentHashMap比HashTable效率要高?
HashTable
使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;ConcurrentHashMap
-
JDK1.7
中使用分段锁(ReentrantLock
+Segment
+HashEntry
),相当于把一个HashMap
分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于Segment
,包含多个HashEntry
。 -
JDK1.8
中使用CAS
+synchronized
+Node
+红黑树。锁粒度:Node
(首结点)(实现Map.Entry<K,V>
)。锁粒度降低了。
18.针对ConcurrentHashMap锁机制具体分析(JDK1.7VSJDK1.8)?
JDK1.7
中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类Segment
和HashEntry
。
-
①、
Segment
继承ReentrantLock
(重入锁)用来充当锁的角色,每个Segment
对象守护每个散列映射表的若干个桶; -
②、
HashEntry
用来封装映射表的键-值对; -
③、每个桶是由若干个
HashEntry
对象链接起来的链表;
JDK1.8
中,采用Node
+CAS
+Synchronized
来保证并发安全。取消类Segment
,直接用table
数组存储键值对;当HashEntry
对象组成的链表长度超过TREEIFY_THRESHOLD
时,链表转换为红黑树,提升性能。底层变更为数组+链表+红黑树。
19.ConcurrentHashMap在JDK1.8中,为什么要使用内置锁synchronized来代替重入锁ReentrantLock?
-
①、粒度降低了;
-
②、
JVM
开发团队没有放弃synchronized
,而且基于JVM
的synchronized
优化空间更大,更加自然。 -
③、在大量的数据操作下,对于
JVM
的内存压力,基于API
的ReentrantLock
会开销更多的内存。
20.ConcurrentHashMap简单介绍?
-
①、重要的常量:
-
private transient volatile intsizeCtl
; -
当为负数时,-1表示正在初始化,-N表示N-1个线程正在进行扩容;
-
当为0时,表示
table
还没有初始化; -
当为其他正数时,表示初始化或者下一次进行扩容的大小。
-
-
②、数据结构:
-
Node
是存储结构的基本单元,继承HashMap
中的Entry
,用于存储数据; -
TreeNode
继承Node
,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据; -
TreeBin
是封装TreeNode
的容器,提供转换红黑树的一些条件和锁的控制。
-
-
③、存储对象时(
put()
方法):-
1.如果没有初始化,就调用
initTable()
方法来进行初始化; -
2.如果没有
hash
冲突就直接CAS
无锁插入; -
3.如果需要扩容,就先进行扩容;
-
4.如果存在
hash
冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入; -
5.如果该链表的数量大于阀值8,就要先转换成红黑树的结构,
break
再一次进入循环 -
6.如果添加成功就调用
addCount()
方法统计size
,并且检查是否需要扩容。
-
-
④、扩容方法
transfer()
: 默认容量为16,扩容时,容量变为原来的两倍。helpTransfer()
: 调用多个工作线程一起帮助进行扩容,这样的效率就会更高。 -
⑤、获取对象时(
get()
方法):-
1.计算hash值,定位到该
table
索引位置,如果是首结点符合就返回; -
2.如果遇到扩容时,会调用标记正在扩容结点
ForwardingNode.find()
方法,查找该结点,匹配就返回; -
3.以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回
null
。
-
21.ConcurrentHashMap的并发度是什么?
程序运行时能够同时更新ConccurentHashMap
且不产生锁竞争的最大线程数。默认为16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap
会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
微信公众号【程序员书单】
一个为程序员推荐好书的公众号。每周为你pick精品书单,优质学习资源和工具软件。关注前沿技术与行业资讯,更关注你的自我提升。人生苦短,要把时间浪费在美好的事物上~
回复“book”即可领取java后端学习必备20+本电子书。更多电子书下载,请移步至程序员书单官网:coderbooklist.com
微信公众号【Java技术江湖】
一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、mysql、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。
以上是关于Java集合详解10:ConcurrentHashmap面试题汇总,最近找工作老是被问!的主要内容,如果未能解决你的问题,请参考以下文章
Java集合详解10:ConcurrentHashmap面试题汇总,最近找工作老是被问!