面试题答案
一键面试并发度的定义与实现
- 定义:在
ConcurrentHashMap
中,并发度(concurrencyLevel)指的是该哈希表能够同时支持的并发更新操作的大致数量。它实际上是ConcurrentHashMap
中所划分的段(Segment)的数量。每个段都是一个独立的哈希表结构,这样不同线程可以同时操作不同的段,从而实现并发访问。 - 实现:在JDK 1.7及以前,
ConcurrentHashMap
由多个Segment
组成,每个Segment
继承自ReentrantLock
,包含一个哈希表数组。concurrencyLevel
在构造函数中初始化,默认值为16,即创建16个Segment
。当向ConcurrentHashMap
中插入或获取元素时,首先通过哈希值定位到具体的Segment
,然后对该Segment
加锁进行操作。在JDK 1.8中,ConcurrentHashMap
摒弃了Segment
的概念,采用数组 + 链表 + 红黑树的数据结构,通过CAS
(Compare And Swap)操作和Synchronized
关键字来保证并发安全。虽然不再有Segment
,但ConcurrentHashMap
的并发度仍然受数组长度等因素影响,数组长度在一定程度上决定了并发操作的粒度。
高并发场景下的优化机制
- 锁分段技术(JDK 1.7及以前):通过将数据分成多个段,不同线程可以同时访问不同段的数据,减少锁竞争。例如,一个线程在更新
Segment[0]
的数据时,另一个线程可以同时读取或更新Segment[1]
的数据,大大提高了并发性能。 - 减小锁粒度(JDK 1.8):在JDK 1.8中,当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查找效率。并且,在插入和删除操作时,采用
CAS
和Synchronized
结合的方式,对单个节点进行操作,而不是像JDK 1.7那样对整个Segment
加锁,进一步减小了锁的粒度,提高了并发性能。 - 读操作无锁化:无论是JDK 1.7还是JDK 1.8,
ConcurrentHashMap
的读操作大部分情况下是无锁的。在JDK 1.7中,Segment
中的数据通过volatile
关键字修饰,保证了可见性,读操作无需加锁。在JDK 1.8中,数组和节点中的数据同样通过volatile
修饰,并且通过Unsafe
类的getObjectVolatile
等方法实现高效的无锁读操作,提高了读性能。 - 扩容优化:在扩容时,JDK 1.8的
ConcurrentHashMap
采用了更优化的方式。它会将数据分散到新的数组中,并且允许在扩容过程中,其他线程继续进行读操作。通过使用ForwardingNode
来标识正在扩容的节点,当读操作遇到ForwardingNode
时,会去新的数组中查找数据,保证了扩容期间的并发性能。