Interview AiBoxInterview AiBox 实时 AI 助手,让你自信应答每一场面试
ConcurrentHashMap的实现原理是什么?
题型摘要
ConcurrentHashMap是Java并发包中的线程安全HashMap实现,旨在提供高并发性和高性能。JDK 1.7及之前采用分段锁(Segment)设计,每个Segment守护一部分数据,不同Segment的操作可并发执行。JDK 1.8及之后改为使用CAS操作和synchronized关键字,锁粒度更细,只锁住需要修改的桶,进一步提高了并发性。ConcurrentHashMap的读操作通常不需要加锁,写操作只锁定必要的部分,支持多线程并发扩容,整体性能远超Hashtable和Collections.synchronizedMap。其实现体现了无锁算法、细粒度锁、并发扩容等并发编程思想,是高性能并发编程的重要组件。
ConcurrentHashMap的实现原理
1. 基本概念和设计目标
ConcurrentHashMap是Java并发包(java.util.concurrent)中的一个线程安全的HashMap实现。它的设计目标是在保证线程安全的同时,提供比Hashtable和Collections.synchronizedMap更高的并发性能。
设计目标
- 高并发性:允许多个线程同时读和一定程度的写操作
- 线程安全:保证多线程环境下数据的一致性
- 高性能:尽可能减少锁竞争,提高并发访问效率
2. JDK 1.7及之前的实现(分段锁)
在JDK 1.7及之前的版本中,ConcurrentHashMap采用**分段锁(Segment)**的设计来实现并发控制。
内部结构
ConcurrentHashMap内部由一个Segment数组组成,每个Segment又包含一个HashEntry数组。Segment是一种可重入锁(ReentrantLock),每个Segment守护着它所包含的HashEntry数组中的元素。
工作原理
-
数据定位:
- 首先通过key的hash值确定在哪个Segment中
- 然后在该Segment中通过再次hash确定具体的HashEntry位置
-
并发控制:
- 写操作需要获取对应Segment的锁
- 读操作不需要加锁(通过volatile保证可见性)
- 不同Segment的写操作可以并发进行
-
容量计算:
- 整个ConcurrentHashMap的容量是所有Segment容量的总和
- 每个Segment的容量默认为16,负载因子为0.75
代码示例
// JDK 1.7 ConcurrentHashMap的put方法简化版
public V put(K key, V value) {
Segment<K,V> s;
// 计算key的hash值,并找到对应的Segment
int hash = hash(key.hashCode());
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 调用Segment的put方法
return s.put(key, hash, value, false);
}
// Segment的put方法简化版
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,如果失败则自旋获取
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(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都维护自己的HashEntry数组)
- 在某些场景下,并发度可能不够高(Segment数量固定)
3. JDK 1.8及之后的实现(CAS + synchronized)
从JDK 1.8开始,ConcurrentHashMap的实现发生了重大变化,放弃了分段锁的设计,转而采用CAS(Compare And Swap)操作和synchronized关键字来实现并发控制。
内部结构
JDK 1.8的ConcurrentHashMap内部结构与HashMap类似,由一个Node数组组成,每个Node可能是一个链表或红黑树的头节点。
工作原理
-
CAS操作:
- 在无竞争的情况下,使用CAS操作来更新节点的值
- CAS是一种无锁算法,通过比较并交换来保证原子性
-
synchronized锁:
- 在有竞争的情况下,使用synchronized锁住对应的桶(Node数组中的某个位置)
- 锁的粒度更细,只锁住需要修改的桶,而不是整个Segment
-
关键优化:
- 扩容优化:支持多线程并发扩容
- 树化优化:当链表长度超过8时,转换为红黑树,提高查询效率
- 计数优化:使用CounterCell来记录元素数量,减少竞争
代码示例
// JDK 1.8 ConcurrentHashMap的putVal方法简化版
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果桶为空,使用CAS操作添加新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用synchronized锁住桶的头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
优缺点
优点:
- 内存占用更少(不需要维护多个Segment)
- 锁粒度更细,并发度更高
- 在高并发场景下性能更好
缺点:
- 实现更复杂,理解难度更高
- 在某些低并发场景下,性能可能不如JDK 1.7的实现
4. 关键操作的实现原理
get操作
-
JDK 1.7:
- 不加锁,通过volatile保证可见性
- 首先定位到Segment,然后在Segment中查找对应的HashEntry
-
JDK 1.8:
- 不加锁,通过volatile保证可见性
- 直接通过hash值定位到对应的桶,然后遍历链表或红黑树
put操作
-
JDK 1.7:
- 首先定位到Segment,然后获取Segment的锁
- 在Segment中执行put操作,可能触发rehash
- 释放锁
-
JDK 1.8:
- 首先定位到桶
- 如果桶为空,使用CAS操作添加新节点
- 如果桶不为空,使用synchronized锁住桶的头节点
- 执行put操作,可能触发树化或扩容
- 释放锁
size操作
-
JDK 1.7:
- 遍历所有Segment,累加它们的count值
- 在累加过程中,可能尝试获取Segment的锁(如果连续两次获取的值不一致)
-
JDK 1.8:
- 使用CounterCell数组来记录元素数量
- 在多线程环境下,不同线程可能更新不同的CounterCell
- 计算size时,累加所有CounterCell的值
扩容操作
-
JDK 1.7:
- 每个Segment独立扩容,不影响其他Segment
- 扩容时获取Segment的锁,阻塞其他线程对该Segment的操作
-
JDK 1.8:
- 支持多线程并发扩容
- 扩容时,将数组分成多个部分,不同线程可以负责迁移不同的部分
- 迁移过程中,读操作可以并发进行,写操作可能会协助扩容
5. 与其他线程安全Map的对比
Hashtable
- 锁机制:使用synchronized关键字锁住整个表
- 性能特点:
- 任何写操作都会阻塞整个表
- 读操作也需要加锁
- 并发度低,性能较差
Collections.synchronizedMap
- 锁机制:使用synchronized关键字锁住整个Map对象
- 性能特点:
- 任何写操作都会阻塞整个Map
- 读操作也需要加锁
- 并发度低,性能较差
- 迭代时需要手动加锁
ConcurrentHashMap
- 锁机制:
- JDK 1.7:分段锁(Segment)
- JDK 1.8:CAS + synchronized
- 性能特点:
- 锁粒度细,只锁住需要修改的部分
- 读操作不需要加锁(通过volatile保证可见性)
- 并发度高,性能较好
- 支持复合操作的原子性(如putIfAbsent)
6. 总结
ConcurrentHashMap是Java并发包中一个重要的线程安全Map实现,它的设计目标是提供高并发性和高性能。
-
JDK 1.7及之前:采用分段锁(Segment)的设计,每个Segment是一个独立的哈希表,拥有自己的锁。这种设计允许多个线程同时访问不同的Segment,提高了并发性。
-
JDK 1.8及之后:放弃了分段锁的设计,转而采用CAS操作和synchronized关键字。锁的粒度更细,只锁住需要修改的桶,进一步提高了并发性。
ConcurrentHashMap的实现原理体现了Java并发编程的一些重要思想,如无锁算法、细粒度锁、并发扩容等。理解ConcurrentHashMap的实现原理,对于编写高性能的并发程序非常有帮助。
参考资料
思维导图
Interview AiBoxInterview AiBox — 面试搭档
不只是准备,更是实时陪练
Interview AiBox 在面试过程中提供实时屏幕提示、AI 模拟面试和智能复盘,让你每一次回答都更有信心。
AI 助读
一键发送到常用 AI
ConcurrentHashMap是Java并发包中的线程安全HashMap实现,旨在提供高并发性和高性能。JDK 1.7及之前采用分段锁(Segment)设计,每个Segment守护一部分数据,不同Segment的操作可并发执行。JDK 1.8及之后改为使用CAS操作和synchronized关键字,锁粒度更细,只锁住需要修改的桶,进一步提高了并发性。ConcurrentHashMap的读操作通常不需要加锁,写操作只锁定必要的部分,支持多线程并发扩容,整体性能远超Hashtable和Collections.synchronizedMap。其实现体现了无锁算法、细粒度锁、并发扩容等并发编程思想,是高性能并发编程的重要组件。
智能总结
深度解读
考点定位
思路启发
相关题目
在软件开发中,如何设计有效的测试用例?
设计有效测试用例需遵循明确性、完整性、独立性等原则,运用等价类划分、边界值分析等黑盒测试技术和语句覆盖、分支覆盖等白盒测试技术。针对单元测试、集成测试、系统测试和验收测试等不同级别,采用相应的设计策略和方法。测试用例应包含完整的文档结构,使用专业工具进行管理,并基于风险分析确定优先级。最佳实践包括测试用例复用、自动化测试和定期评审,避免过度依赖脚本、忽视负面测试等常见误区。
请详细说明ArrayList和LinkedList的区别,包括它们的底层实现、性能特点和使用场景。
ArrayList和LinkedList是Java中两种常用的List实现,它们在底层实现、性能特点和使用场景上有显著差异。ArrayList基于动态数组实现,具有O(1)的随机访问性能,但插入/删除操作需要移动元素,时间复杂度为O(n);LinkedList基于双向链表实现,随机访问性能为O(n),但插入/删除操作只需修改指针,时间复杂度为O(1)。ArrayList适合读多写少、需要频繁随机访问的场景;LinkedList适合写多读少、需要频繁在头部或中间插入/删除的场景,同时它还实现了Deque接口,可作为队列或双端队列使用。在实际开发中,ArrayList的使用频率更高,因为大多数场景下随机访问的需求更常见,且内存效率更高。
HashMap的底层原理是什么?它是线程安全的吗?在多线程环境下会遇到什么问题?如果要保证线程安全应该使用什么?ConcurrentHashMap是怎么保证线程安全的?请详细说明。
HashMap基于数组+链表/红黑树实现,通过哈希函数计算元素位置,使用链地址法解决哈希冲突。HashMap是非线程安全的,多线程环境下可能导致死循环、数据覆盖等问题。线程安全的替代方案包括Hashtable、Collections.synchronizedMap()和ConcurrentHashMap。ConcurrentHashMap在JDK 1.7采用分段锁实现,JDK 1.8改用CAS+synchronized,锁粒度更细,并发性能更好。
Java中的集合框架(Collection & Map)有哪些主要接口和实现类?
Java集合框架主要分为Collection和Map两大体系。Collection体系包括List(有序可重复,如ArrayList、LinkedList)、Set(无序不可重复,如HashSet、TreeSet)和Queue(队列,如PriorityQueue、ArrayDeque)。Map体系存储键值对,主要实现类有HashMap、LinkedHashMap、TreeMap、Hashtable和ConcurrentHashMap等。不同集合类在底层结构、有序性、线程安全、时间复杂度等方面有不同特性,应根据具体需求选择合适的实现类。
请详细介绍一下你参与过的项目,包括项目背景、你的职责以及使用的技术栈。
面试者需要清晰介绍参与过的项目,包括项目背景、个人职责、使用的技术栈、遇到的挑战及解决方案,以及项目成果和个人收获。重点突出自己在项目中的具体贡献、技术选型的思考过程、解决问题的思路以及从中获得的成长。回答应结构清晰,重点突出,体现技术深度和解决问题的能力。