
1. 常见的 Map 集合(非线程安全)
这类集合性能极高,但在多线程环境下会出现数据丢失或死循环等问题。
| 集合名称 | 底层数据结构 | 核心特点 | 适用场景 |
|---|---|---|---|
HashMap | 数组 + 链表 + 红黑树 (JDK 1.8) | 通过哈希算法快速存取;不保证任何顺序。 | 最通用的键值对存储。 |
LinkedHashMap | 继承 HashMap + 双向链表 | 维护了元素的插入顺序或访问顺序。 | 需要按插入先后顺序遍历,或实现 LRU 缓存。 |
TreeMap | 红黑树 | 自动对 Key 进行排序(自然顺序或自定义比较器)。 | 需要按键的大小范围进行查询或排序。 |
2. *常见的 Map 集合(线程安全)
在并发环境下,面试官更看重你对 ConcurrentHashMap 细粒度锁的理解。
-
Hashtable(早期遗留):-
实现: 数组 + 链表。
-
机制: 在每个方法上直接加
synchronized。 -
缺点: 锁住的是整个对象(全表锁),多线程竞争时性能极低,已很少被推荐使用。
-
-
ConcurrentHashMap(现代并发首选):-
实现: Node 数组 + 链表 + 红黑树。
-
机制: JDK 1.8 舍弃了分段锁,改为 volatile + CAS + synchronized 锁住桶的头节点(行级锁),大幅减小了并发冲突。
-
地位: 高并发场景下
Hashtable的完美替代品。
-
-
ConcurrentSkipListMap:-
实现: 基于跳表(SkipList)算法。
-
特点: 线程安全的有序 Map。
-
场景: 在高并发环境下且需要对键进行排序时使用。
-
3.如何对map进行快速遍历?
在 Java 集合的语境下,Entry 的中文翻译通常是 “条目” 或 “项”。
| 遍历方法 | 代码风格 | 性能表现 | 适用场景 |
|---|---|---|---|
entrySet() + for-each | 传统命令式 | 最高 | 最推荐。同时需要 Key 和 Value 时使用。 |
forEach() + Lambda | 现代函数式 | 高 | Java 8+ 首选。语法最简洁。 |
keySet() + for-each | 简单直观 | 中/低 | 仅当只需要 Key 时使用。若再调用 get(key) 会导致二次哈希查找。 |
Iterator (迭代器) | 底层控制 | 中 | 遍历过程中需要删除元素时使用。 |
| Stream API | 管道流式 | 中 | 需要对结果进行过滤、转换或聚合操作时使用。 |
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
if (某种条件) it.remove(); // 安全删除
}“在实际生产中,如果同时需要键值,我会坚决避免使用 keySet() 配合 map.get(key),因为这会产生额外的哈希寻址开销。对于 90% 的普通业务,我会直接用 Java 8 的 forEach;如果涉及复杂的过滤和转换,我会选择 Stream API。”
4.*HashMap实现原理介绍一下?
1. HashMap 的核心结构
HashMap 是一个基于哈希表实现的键值对存储容器。它的底层结构经历了从 JDK 1.7 到 1.8 的重要演进:
-
JDK 1.7 及以前:由数组 + 链表组成。
- 使用“拉链法”解决哈希冲突,将冲突的元素存在同一个数组槽位(Bucket)的链表中。
-
JDK 1.8 及以后:由数组 + 链表 + 红黑树组成。
- 当链表长度过长时,会转化为红黑树,以大幅提升查询效率。

- 当链表长度过长时,会转化为红黑树,以大幅提升查询效率。
2. 核心工作流程
-
哈希计算:通过
hash(key)计算键的哈希值,再通过取模运算(通常是(n-1) & hash)确定在数组中的下标。 -
处理冲突:如果该位置已有元素,则以链表形式挂在后面。
-
树化机制(JDK 1.8 重点):
-
当某个桶位的链表长度大于 8 时,系统会尝试将链表转换为红黑树。
-
注意点:转换前还会检查数组长度,只有当数组总长度大于 64 时才会执行树化,否则优先选择扩容。
-

3. *为什么要引入红黑树?
-
性能提升:链表的查询时间复杂度是 $O(n)$,而红黑树是 $O(\log n)$。
-
安全性:在发生极端哈希冲突(如遭受 Hash 碰撞攻击)时,链表会变得非常长,导致性能急剧下降。引入红黑树能保证在最坏情况下,查询依然高效。
5.HashMap链表发生转换后为什么不用平衡二叉树?
红黑树在插入和删除操作上的性能表现更均衡。 虽然平衡二叉树(AVL)是严格平衡的,查询效率极高,但 HashMap 作为一个频繁进行增删操作的容器,红黑树的维护成本更低。
“简单来说,AVL 树是‘重查询、轻增删’,而红黑树是‘兼顾查询与增删’。 HashMap 作为一个需要应对各种复杂业务场景的散列表,必须在读写之间找到一个平衡点。红黑树通过降低平衡的严苛程度,换取了更高效的插入和删除性能,这更符合 HashMap 高频动态变化的特性。”
6.了解的哈希冲突解决方法有哪些?
| 方法 | 核心优势 | 致命缺点 | Java 典型应用 |
|---|---|---|---|
| 拉链法 | 扩展性强,支持大量数据 | 链表过长性能退化 | HashMap / Hashtable |
| 开放地址法 | 内存利用率高(不需额外指针) | 容易堆积,删除逻辑复杂 | ThreadLocalMap |
| 再 hash | 计算高 | ||
| 建立公共溢出区 |
“在面试中,我会追问你:‘为什么 HashMap 不用开放地址法?’你可以回答:因为开放地址法更容易产生冲突聚集,且当装载因子较高时性能急剧下降;而拉链法配合 JDK 1.8 的红黑树优化,能提供更稳定的 $O(\log n)$ 性能兜底。”
7.**HashMap是线程安全的吗?
1. 核心结论:绝对不是
HashMap 是非线程安全的。在多线程环境下,没有任何同步机制来保护其内部数据的完整性。
2. 为什么不安全?(具体表现)
面试官最希望听到以下三个维度的风险:
① 数据覆盖(Data Loss)
当两个线程同时调用 put() 操作,且计算出的哈希槽位(Bucket)相同时:
-
线程 A 和线程 B 同时判断该位置为 null。
-
线程 A 写入后,线程 B 接着写入,导致线程 A 的数据被彻底覆盖。
**② size 不准确
size++ 并不是一个原子操作。它包含“读取-修改-写入”三步,多线程下会导致最终记录的元素个数小于实际存入的个数。
③ 死循环问题(JDK 1.7 遗留风险)
-
背景:在 JDK 1.7 中,
HashMap扩容采用的是“头插法”。 -
现象:当多个线程同时触发扩容时,可能会导致链表形成环形结构。
-
后果:后续调用
get()查找该位置的元素时,程序会进入死循环,导致 CPU 飙升至 100%。 -
注意:JDK 1.8 采用了“尾插法”修复了死循环问题,但在并发下依然会出现数据丢失。
3. 如何解决线程安全问题?
如果你需要一个线程安全的 Map,面试官通常会看你的选型优先级:
| 方案 | 实现机制 | 性能 | 面试官点评 |
|---|---|---|---|
| Hashtable | 方法级 synchronized | 极差 | 整个表一把大锁,现代开发基本弃用。 |
| Collections.synchronizedMap | 包装类,加同步块 | 差 | 同样是全表锁,适合并发极低且需要包装现有 Map 的场景。 |
| ConcurrentHashMap | CAS + synchronized (1.8) | 极高 | 大厂首选。锁粒度细化到每个桶的头节点,支持高并发并发读写。 |
| 面试官喜欢的“避坑”总结: |
“即便在 JDK 1.8 中修复了扩容死循环的问题,
HashMap依然不能在多线程环境下使用。因为其add操作不具备原子性,volatile缺失导致不可见性,这些都会导致数据错乱。在高并发场景下,ConcurrentHashMap是唯一的工业级标准选型。”
8.**hashmap的put过程介绍一下
第一步:计算哈希值
并不是直接使用 key.hashCode(),而是调用内部的 hash() 方法。
-
扰动函数:
(h = key.hashCode()) ^ (h >>> 16)。 -
目的:将高 16 位与低 16 位进行异或运算,使得哈希值更加散列,减少在数组较小时的哈希冲突。
第二步:检查并初始化数组
- 如果底层的
Node[] table为null或长度为 0,会立刻调用resize()方法进行初始化扩容(默认初始容量为 16)。
第三步:计算桶位(Index)
- 使用公式
(n - 1) & hash计算出该元素在数组中的下标。
第四步:插入处理(分三种情况)
-
无冲突:如果该下标对应的桶位为空,直接新建节点(Node)放入其中。
-
Key 相同(也就是同一个数):如果桶位中第一个节点的 Key 与待插入的 Key 相同(通过
==或equals判定),则准备进行 Value 覆盖。 -
发生冲突(碰撞)(不是同一个数):
-
如果是红黑树:调用
putTreeVal方法,按照红黑树的规则插入节点。 -
如果是链表:遍历链表,在尾部插入新节点(JDK 1.8 采用尾插法)。
- 树化检查:插入后,如果链表长度达到 8,且数组总长度达到 64,则将链表转换为红黑树。
-
第五步:Value 覆盖与返回
- 如果发现 Key 已存在,根据
onlyIfAbsent参数决定是否用新值覆盖旧值,并返回旧值。
第六步:更新 Size 与扩容检查
-
插入成功后,
size加 1。 -
扩容判定:如果当前
size超过了threshold(容量 $\times$ 加载因子,默认 $16 \times 0.75 = 12$),则触发resize()进行扩容。
9.HashMap的put(key,val)和get(key)过程
put在上一回答
get 的过程相对简单,但在查找过程中需要兼容链表和红黑树两种结构。
-
计算 Hash 值:
同样先通过扰动函数计算 Key 的哈希值,定位到对应的数组下标。
-
检查桶位第一个节点:
如果桶位为空,直接返回 null。
如果桶位第一个节点的 Key 恰好与查询 Key 相等,直接返回该节点的值。
-
查找后续节点:
如果第一个节点不匹配,且存在后续节点(next != null):
-
如果是红黑树:调用
getTreeNode,在红黑树中通过二分查找逻辑定位,复杂度为 $O(\log n)$。 -
如果是链表:按顺序遍历链表,通过
equals逐一比对 Key,复杂度为 $O(n)$。
-
-
返回结果:
找到则返回 Value,遍历结束仍未找到则返回 null。
10.**hashmap 调用get方法一定安全吗?
① 扩容期间可能读到 null
这是面试中最常考的点。当 HashMap 进行 resize(扩容)时,会创建一个新数组并把旧数据迁移过去。
-
过程:在迁移过程中,某个桶(Bucket)位的数据可能已经被清空,但还没来得及放入新数组。
-
后果:如果此时另一个线程调用
get查询这个位置的 Key,即使该 Key 确实存在,get也会返回null。这种现象被称为数据丢失或幻读。
② 内存可见性问题
HashMap 内部的数组 table 和节点的 value 都没有使用 volatile 关键字修饰。
- 后果:线程 A 修改了某个 Key 的 Value,线程 B 在另一个 CPU 核心上执行
get时,可能由于 CPU 缓存未同步而读到旧值。
③ 死循环风险(针对 JDK 1.7)
-
现象:在 JDK 1.7 中,并发扩容采用“头插法”,会导致链表形成环形结构。
-
后果:当
get访问到这个环形链表并试图遍历查找 Key 时,会陷入 CPU 100% 的死循环。虽然 JDK 1.8 改用尾插法解决了死循环,但上述的数据不一致问题依然存在。
| 方案 | 安全机制 | 优点 |
|---|---|---|
ConcurrentHashMap | CAS + synchronized + volatile | 首选方案。读操作不加锁,且通过 volatile 保证了强可见性,不会读到扩容中途的错误状态。 |
Collections.synchronizedMap | 对象级排他锁 | 简单直接,但并发性能差(读读也互斥)。 |
Hashtable | 方法级 synchronized | 遗留类,性能差,不建议使用。 |
11.HashMap一般用什么做Key?为啥String适合做Key呢?
String 在 Java 中是不可变的,一旦创建,它的内容就不能被修改。
| 维度 | String 做 Key | 可变对象(如自定义类)做 Key |
|---|---|---|
| 安全性 | 高。内容不改,位置不变。 | 低。属性一变,数据即“失踪”。 |
| 查询性能 | 极快。支持 Hash 缓存。 | 较慢。每次都要重新计算。 |
| 复杂度 | 简单,直接使用。 | 复杂,必须严格重写 hashCode 和 equals。 |
“其实不仅仅是 String,像 Integer、Long 等包装类也很适合做 Key,因为它们同样是 Final(不可变) 的。在实际开发中,如果你必须用自定义对象做 Key,请务必保证:1. 它是不可变的;2. 必须同时重写 hashCode 和 equals。” |
12.为什么HashMap要用红黑树而不是平衡二叉树?
13.hashmap key可以为null吗?
- 核心结论
-
HashMap支持null键和null值。 -
它最多只允许 一个
null键(因为 Key 必须唯一),但可以有多个null值。
- 底层是如何处理
null键的?
在调用 put(K key, V value) 时,HashMap 会先调用内部的 hash(key) 方法。我们来看一下源码逻辑:
Java
static final int hash(Object key) {
int h;
// 如果 key 为 null,直接返回哈希值 0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
固定位置:由于
null的哈希值被强制设为 0,经过下标运算(n - 1) & hash后,结果永远是 0。 -
存储位置:因此,
HashMap的null键总是存储在底层数组的 第一个桶(下标为 0 的位置)。
___14.重写HashMap的equal和hashcode方法需要注意什么?
“简单来说,hashCode 决定了你在哪个‘抽屉’找,equals 决定了你在这个‘抽屉’里拿的是不是你要的。如果不成对重写,就会出现‘找错了抽屉’或者‘明明在抽屉里却认不出它’的情况。在实际开发中,这通常是导致缓存失效或内存泄漏的隐蔽原因。”
- 如果
a.equals(b)为 true,那么a.hashCode()必须等于b.hashCode()。 - 如果
a.hashCode() == b.hashCode(),a.equals(b)不一定为 true(这就是哈希冲突)。
在 HashMap 中的工作原理
HashMap 查找或插入一个元素分为两步:
-
第一步:找桶位。 调用
key.hashCode()计算哈希值,定位到数组的某个下标。 -
第二步:找 Key。 在该下标对应的链表或红黑树中,利用
key.equals(existingKey)逐个比对,直到找到完全匹配的那一个。
*重写时的 4 个注意事项
-
一致性字段:参与
equals比较的属性,必须全部参与hashCode的计算。否则会导致equals为 true 但hashCode不同的尴尬局面。 -
不可变性优先:尽量使用对象中
final修饰的、不可变的字段。如果参与计算的属性在存入 Map 后被修改了,该对象在 Map 中就“失踪”了(因为哈希值变了,你再也找不回原来的桶)。 -
利用工具类:为了防止空指针异常并提高代码可读性,建议使用
Objects.hash(field1, field2)和Objects.equals(a, b)。 -
性能考量:
hashCode会在每次put/get时调用,计算逻辑不宜过于复杂,且应尽量保证散列分布均匀。
___15.重写HashMap的equal方法不当会出现什么问题?
| 错误类型 | 具体表现 | 产生的后果 |
|---|---|---|
| 逻辑太窄 | 对象内容相同但 equals 为 false | 产生重复 Key,数据冗余。 |
| 逻辑太宽 | 不同对象 equals 却为 true | 导致错误地覆盖了别人的数据(数据丢失)。 |
| 不一致性 | equals 为 true 但 hashCode 不同 | 存入后无法 get 到,找不到桶位。 |
| 依赖易变字段 | 存入后修改了参与 equals 的属性 | 导致 Key 在 Map 中“失踪”,无法被索引或删除。 |
-
契约高于一切:一定要遵守“
equals为 true,hashCode必须相等”的铁律。 -
使用不可变字段:尽量只用
final字段参与equals和hashCode的计算。 -
防御性编程:在重写
equals时,先进行this == obj的引用判断,再进行instanceof或getClass的类型检查,最后比对核心属性。
16.HashMap的扩容机制介绍一下
“HashMap 的扩容通过翻倍策略和高低位迁移技巧,实现了非常优雅的数据分布。JDK 1.8 的尾插法优化不仅提升了迁移效率,更重要的是消除了多线程下的环形链表死循环风险,虽然它依然不是线程安全的。”
扩容时,新数组的容量永远是原数组的 2 倍。
-
为什么要翻倍? 1. 为了保证容量始终是 2 的 n 次方。
- 利用位运算优化:计算下标时使用 (n-1) & hash,当 $n$ 是 2 的幂时,这等价于取模运算,但效率更高。
17.HashMap的大小为什么是2的n次方大小呢?
- 将取模运算优化为位运算(性能极致)
在常规逻辑中,为了将 Hash 值映射到数组下标,通常使用取模运算:index = hash % length。
但是,取模运算(%)在 CPU 层面是非常耗时的操作。
- 位运算优化:当容量 $n$ 是 $2$ 的幂次方时,满足公式: $$(n - 1) \ & \ hash == hash % n$$
- 效率差异:位运算符
&(与运算)的执行效率远高于%。由于HashMap的get和put极其频繁,这种底层优化能显著提升整体吞吐量。
- 保证哈希值的均匀分布(减少碰撞)
如果数组长度 $n$ 是 $2^n$,那么 $n - 1$ 的二进制表示一定是全 1(例如 $16-1=15$,二进制为 1111)。
-
均匀映射:当执行
hash & (1111)时,Hash 值的最后四位能完整地反映在下标上。 -
反面教材:如果长度是奇数(比如 15),$n-1$ 为 14(二进制
1110)。最后一位永远是 0。这意味着下标只能是偶数,浪费了数组一半的空间,冲突概率瞬间翻倍。
18.往hashmap存20个元素,会扩容几次?
| 阶段 | 数组容量 (Capacity) | 触发扩容的阈值 (Threshold) | 备注 |
|---|---|---|---|
| 初始状态 | 0 | 0 | 懒加载,尚未分配内存 |
| 第 1 次 put | 16 | 12 | 第一次调用 resize() 进行初始化 |
| 第 13 次 put | 32 | 24 | 第二次调用 resize(),容量翻倍 |
| 第 20 次 put | 32 | 24 | 此时 $20 < 24$,无需扩容 |
19.说说hashmap的负载因子
- 什么是负载因子(Load Factor)?
负载因子是 HashMap 的一个扩容参数,用来衡量哈希表的“满”程度。
-
计算公式:$\text{threshold (阈值)} = \text{capacity (容量)} \times \text{loadFactor (负载因子)}$
-
默认值:Java 官方将其默认值设为 0.75。
-
意义:当
HashMap中的元素个数超过这个阈值时,就会触发resize()操作(容量翻倍并重新哈希)。
“负载因子的默认值 0.75 是 Java 开发团队在大量实验和统计学基础上得出的最佳实践。它在单次查询的时间开销和整体内存占用之间取得了完美的平衡。在大多数实际项目中,我们不建议手动修改这个值,而应该通过预估数据量来合理设置 initialCapacity(初始容量),这才是真正的调优手段。”
20.Hashmap和Hashtable有什么不一样的?Hashmap一般怎么用?
| 特性 | HashMap (现代选型) | Hashtable (遗留类) |
|---|---|---|
| 线程安全性 | 非线程安全。不支持多线程并发操作。 | 线程安全。通过在方法上加 synchronized 实现。 |
| Null 支持 | 支持 1 个 null 键和多个 null 值。 | 不支持。Key 或 Value 为 null 均抛出 NPE。 |
| 性能 | 高。没有锁开销,是目前最高效的 Map。 | 低。由于全表锁竞争,并发环境下性能极差。 |
| 底层结构 | 数组 + 链表 + 红黑树 (JDK 1.8+)。 | 数组 + 链表。始终没有红黑树优化。 |
| 初始容量/扩容 | 默认 16,扩容翻倍 ($2n$)。 | 默认 11,扩容为 $2n+1$。 |
| 继承体系 | 继承自 AbstractMap。 | 继承自陈旧的 Dictionary 类。 |
| HashMap主要用来存储键值对,可以调用put方法向其中加入元素,调用get方法获取某个键对应的值,也可以通过containsKey方法查看某个键是否存在等 |
21.ConcurrentHashMap怎么实现的?

| 特性 | JDK 1.7 (Segment) | JDK 1.8 (Node) |
|---|---|---|
| 锁粒度 | 分段级(Segment) | 桶位级(Node/Entry) |
| 锁性能 | 基于 ReentrantLock,开销较大 | 基于 synchronized,且经过了 JVM 大量优化 |
| 查询效率 | 链表查询 $O(n)$ | 红黑树查询 $O(\log n)$ |
| 并发度 | 受限于 Segment 数量 | 受限于数组长度(桶位越多,并发越高) |
7.synchronized和reentrantlock及其应用场景?
关键技术点:为什么查询不需要加锁?
这是 ConcurrentHashMap 性能优异的另一个秘诀。它的 get 操作是完全无锁的。
-
volatile 关键字:
Node节点中的val和next指针都使用了volatile修饰。 -
内存可见性:
volatile确保了当一个线程修改了节点的值,其他线程能够立刻看到最新的值。因此,读线程不需要加锁也能拿到正确的数据。
ConcurrentHashMap 的演进过程其实就是不断追求更细粒度锁的过程。JDK 1.8 放弃了分段锁,转而采用 CAS 和 synchronized 的组合,不仅简化了结构,更让锁的冲突概率降到了最低。同时,它巧妙地利用 volatile 保证了读操作的无锁化,在高并发下的吞吐量远超 Hashtable 和 1.7 版本的实现。
22.分段锁怎么加锁的?
我们实际上是在讨论 JDK 1.7 版本的 ConcurrentHashMap。虽然 JDK 1.8 已经改用了更细粒度的 CAS + synchronized,但分段锁的设计思想在分布式数据库和某些中间件中依然被广泛借鉴。
在 ConcurrentHashMap 中,将整个数据结构分为多个 Segment,每个 Segment 都类似于一个小的 HashMap,每个 Segment 都有自己的锁,不同 Segment 之间的操作互不影响,从而提高并发性能。 在 ConcurrentHashMap 中,对于插入、更新、删除等操作,需要先定位到具体的 Segment,然后再在该 Segment 上加锁,而不是像传统的 HashMap 一样对整个数据结构加锁。这样可以使得不同 Segment 之间的操作并行进行,提高了并发性能。
23.分段锁是可重入的吗?
当然,看名字即可(毕竟他继承了 reentrantLock
24.***已经用了synchronized,为什么还要用CAS呢?
| 场景 | 使用技术 | 原因 |
|---|---|---|
| 桶位为空 (第一位插入) | CAS | 简单、无锁、速度极快,避免线程上下文切换。 |
| 桶位不为空 (有冲突) | synchronized | 业务逻辑复杂,锁住头节点可保证多步骤操作的原子性。 |
| 查询操作 (get) | volatile | 读操作完全不需要 CAS 和 synchronized,通过内存可见性实现无锁读。 |
| 为什么要这样设计?(面试官最想听的总结) |
“为了在并发吞吐量和系统复杂性之间达到平衡。”
-
降低锁竞争:由于只有在发生哈希冲突时才加锁,而
ConcurrentHashMap的数组很大,冲突概率并不高,所以大部分时间是在用 CAS 跑,整体吞吐量极高。 -
减少系统开销:如果全部用
synchronized,初始化空桶时也会有加锁成本;如果全部用CAS,处理红黑树插入时的循环重试会导致 CPU 飙升。
25.ConcurrentHashMap用了悲观锁还是乐观锁?
| 场景 | 锁类型 | 具体技术 | 核心优势 |
|---|---|---|---|
| 桶位为空 (Node == null) | 乐观锁 | CAS | 无锁化,响应速度极快 |
| 发生碰撞 (Node != null) | 悲观锁 | synchronized | 保证复杂逻辑(如树化、链表插入)的原子性 |
| 数据读取 (get) | 无锁 | volatile | 依靠内存可见性,完全不加锁,读性能极高 |
26.HashTable 底层实现原理是什么?
- 底层数据结构:数组 + 链表
- 线程安全实现:全表锁(Synchronized)
-
机制:几乎所有的公共方法(如
put、get、remove、size)都直接加上了synchronized关键字。 -
锁粒度:它锁住的是整个对象实例。
-
后果:同一时间只能有一个线程进行读或写操作。即使两个线程操作的是数组中完全不同的桶位,也必须排队等待,并发性能极低。
-
27.hashtable 和concurrentHashMap有什么区别
| 特性 | Hashtable (旧时代) | ConcurrentHashMap (现代并发) |
|---|---|---|
| 线程安全机制 | 全表锁:对整个对象加 synchronized。 | 细粒度锁:JDK 1.8 使用 CAS + synchronized 锁住桶位头节点。 |
| 并发性能 | 极低:多线程竞争同一把锁,读写全排队。 | 极高:支持多线程并发读写不同桶位,读操作无锁。 |
| 底层结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 (JDK 1.8+) |
| Null 支持 | 不允许 Key 或 Value 为 null。 | 不允许 Key 或 Value 为 null(避免并发二义性)。 |
| 扩容机制 | 11 → 2n + 1 | 16 → 2n (翻倍) |
| 历史地位 | 继承 Dictionary,属于遗留类(Legacy)。 | 继承 AbstractMap,是 util.concurrent 包核心。 |
28.说一下HashMap和Hashtable、ConcurrentMap的区别
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 不安全 | 安全 (全表锁) | 安全 (细粒度锁) |
| 性能 | 极高 (无锁开销) | 极低 (竞争激烈) | 高 (支持高并发读写) |
| Null 支持 | 支持 (1个Key, 多个Value) | 不支持 (抛 NPE) | 不支持 (抛 NPE) |
| 底层结构 | 数组+链表+红黑树 (1.8) | 数组+链表 | 数组+链表+红黑树 (1.8) |
| 实现机制 | 没有任何同步机制 | 方法级 synchronized | CAS + synchronized (1.8) |
| 迭代器 | Fail-Fast (快速失败) | Enumeration / Fail-Fast | Fail-Safe (弱一致性) |