面试准备-Java集合

目录

一、ConcurrentHashMap

1、实现原理

(1)ConcurrentHashMap 的实现原理是什么?

(2)JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock? 

2、存取

(1)ConcurrentHashMap 的 put 方法执行逻辑是什么?

(2)ConcurrentHashMap 的 get 方法执行逻辑是什么?

 (3)ConcurrentHashMap 的 get 方法是否要加锁,为什么?

(4)get 方法不需要加锁与 volatile 修饰的哈希桶数组有关吗?

3、其它

(1)ConcurrentHashMap 不支持 key 或者 value 为 null 的原因? 

(2)ConcurrentHashMap 的并发度是什么?

(3)ConcurrentHashMap 迭代器是强一致性还是弱一致性?

(4)JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?

(5)ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?

(6)具体说一下Hashtable的锁机制

(7)多线程下安全的操作 map还有其他方法吗?

二、HashMap

1、 存储结构

(1)HashMap的底层数据结构是什么?

(2)为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

(3)不用红黑树,用二叉查找树可以么?

(4)为什么链表改为红黑树的阈值是 8?

2、字段结构

(1)默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

3、索引计算

(1)HashMap 中 key 的存储索引是怎么计算的?

(2)JDK1.8 为什么要 hashcode 异或其右移十六位的值?

(3)为什么 hash 值要与length-1相与?

(4)HashMap数组的长度为什么是 2 的幂次方?

(5)补充数组容量计算的小奥秘

4、put方法

(1)HashMap 的put方法流程?

 (2)JDK1.7 和1.8 的put方法区别是什么?

5、扩容机制

(1)HashMap 的扩容方式?

(2)JDK1.8的优化

6、 其他

 (1)还知道哪些hash算法?

(2)key 可以为 Null 吗?

(3)一般用什么作为HashMap的key?

(4)用可变类当 HashMap 的 key 有什么问题?

三、HashMap的线程安全问题

1、多线程下扩容死循环

 2、多线程的put可能导致元素的丢失

3、put和get并发时,可能导致get为null


时隔三个月,我又回来整理学习笔记了。因为准备期末考试和找实习的事,有点小懈怠了。现在实习已经有两个月了,接下来的时间,我会继续学习,继续整理自己每周的学习内容。

一、ConcurrentHashMap

1、实现原理

(1)ConcurrentHashMap 的实现原理是什么?

ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不同的。

先来看下JDK1.7

JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。

存放元素的 HashEntry,也是一个静态内部类,主要的组成如下:

其中,用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性

再来看下JDK1.8

在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用 CAS + synchronized实现更加细粒度的锁。

将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

(2)JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock? 

  • 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

2、存取

(1)ConcurrentHashMap 的 put 方法执行逻辑是什么?

先来看JDK1.7

先定位到相应的 Segment ,然后再进行 put 操作。

源代码如下:

首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

再来看JDK1.8

大致可以分为以下步骤:

  1. 根据 key 计算出 hash 值;

  2. 判断是否需要进行初始化;

  3. 定位到 Node,拿到首节点 f,判断首节点 f:

    • 如果为 null ,则通过 CAS 的方式尝试添加;
    • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
    • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
  4. 当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。

源代码如下:

(2)ConcurrentHashMap 的 get 方法执行逻辑是什么?

同样,先来看JDK1.7

首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。

由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。

源代码如下:

再来看JDK1.8

大致可以分为以下步骤:

  1. 根据 key 计算出 hash 值,判断数组是否为空;

  2. 如果是首节点,就直接返回;

  3. 如果是红黑树结构,就从红黑树里面查询;

  4. 如果是链表结构,循环遍历判断。

源代码如下:

 (3)ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。

这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 效率高的原因之一。

(4)get 方法不需要加锁与 volatile 修饰的哈希桶数组有关吗?

没有关系。哈希桶数组table用 volatile 修饰主要是保证在数组扩容的时候保证可见性。

3、其它

(1)ConcurrentHashMap 不支持 key 或者 value 为 null 的原因? 

先来说value 为什么不能为 null。因为 ConcurrentHashMap 是用于多线程的 ,如果ConcurrentHashMap.get(key)得到了 null ,这就无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,就有了二义性。而用于单线程状态的 HashMap 却可以用containsKey(key) 去判断到底是否包含了这个 null

至于 ConcurrentHashMap 中的 key 为什么也不能为 null 的问题,源码就是这样写的。如果面试官不满意,就回答因为作者Doug不喜欢 null ,所以在设计之初就不允许了 null 的 key 存在。想要深入了解的小伙伴,可以看这篇文章这道面试题我真不知道面试官想要的回答是什么

(2)ConcurrentHashMap 的并发度是什么?

并发度可以理解为程序运行时能够同时更新 ConccurentHashMap且不产生锁竞争的最大线程数。在JDK1.7中,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认是16,这个值可以在构造函数中设置。

如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

在JDK1.8中,已经摒弃了Segment的概念,选择了Node数组+链表+红黑树结构,并发度大小依赖于数组的大小。

(3)ConcurrentHashMap 迭代器是强一致性还是弱一致性?

与 HashMap 迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。

ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。想要深入了解的小伙伴,可以看这篇文章:为什么ConcurrentHashMap是弱一致的

(4)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)。

(5)ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?

ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。

(6)具体说一下Hashtable的锁机制

Hashtable 是使用 synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!

(7)多线程下安全的操作 map还有其他方法吗?

还可以使用Collections.synchronizedMap方法,对方法进行加同步锁。

 如果传入的是 HashMap 对象

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值