MST

星途 面试题库

面试题:Java集合框架并发容器源码研究

以ConcurrentHashMap为例,从源码角度分析其在高并发场景下是如何保证线程安全的,比较其与Hashtable在实现线程安全机制上的差异,并说明ConcurrentHashMap在不同JDK版本中的改进及优化思路。
46.5万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

ConcurrentHashMap线程安全机制(基于源码分析)

  1. JDK 1.7
    • 分段锁机制:将数据分成多个段(Segment),每个段都有自己的锁。当对某个段进行操作时,只需要获取该段的锁,而其他段仍然可以被并发访问。例如,假设有16个Segment,不同线程可以同时访问不同的Segment,大大提高了并发度。
    • put操作:首先通过哈希算法定位到具体的Segment,然后获取该Segment的锁,再进行插入操作。这样在高并发场景下,只要不同线程操作的是不同的Segment,就不会产生锁竞争。
    • get操作:大多数情况下不需要加锁,因为HashEntry的value使用了volatile关键字修饰,保证了可见性。所以直接通过哈希定位到具体位置即可获取数据,提高了读操作的性能。
  2. JDK 1.8
    • 摒弃分段锁,采用CAS + synchronized:在JDK 1.8中,不再使用分段锁,而是直接对数组的每个桶(Node)进行同步控制。当桶为空时,使用CAS操作进行插入;当桶不为空且链表长度小于阈值(默认为8)时,使用synchronized对链表头节点进行同步;当链表长度大于等于阈值时,会将链表转换为红黑树,同样使用synchronized对树节点进行同步。
    • put操作:首先计算哈希值,定位到数组位置。如果该位置为空,使用CAS尝试插入;如果不为空,对该位置的节点加锁(synchronized),然后进行插入或更新操作。如果链表长度达到阈值,会将链表转换为红黑树以提高查找效率。
    • get操作:同样不需要加锁,通过哈希值定位到数组位置,然后直接获取数据。由于Node的value和next都使用了volatile修饰,保证了数据的可见性。

与Hashtable线程安全机制的差异

  1. 锁的粒度
    • Hashtable:对整个哈希表加锁,即所有的读写操作都需要获取同一个锁,在高并发场景下,锁竞争激烈,性能较低。例如,当一个线程在进行put操作时,其他线程无论是读还是写都必须等待锁的释放。
    • ConcurrentHashMap:JDK 1.7采用分段锁,锁的粒度更细,不同段之间可以并发操作;JDK 1.8进一步优化,对每个桶进行同步控制,并发度更高。
  2. 读操作是否加锁
    • Hashtable:读操作也需要加锁,因为其内部没有对数据的可见性做特殊处理,为了保证数据一致性,读操作必须在锁的保护下进行。
    • ConcurrentHashMap:JDK 1.7和1.8的读操作大多情况下都不需要加锁,通过volatile关键字保证了数据的可见性,提高了读性能。
  3. 扩容机制
    • Hashtable:扩容时会锁住整个哈希表,其他线程无法进行操作。
    • ConcurrentHashMap:JDK 1.7在扩容时,每个Segment独立扩容,不会影响其他Segment的操作;JDK 1.8在扩容时,采用了更复杂的机制,允许并发扩容,部分线程可以在扩容过程中继续进行读写操作。

ConcurrentHashMap在不同JDK版本中的改进及优化思路

  1. 从JDK 1.7到JDK 1.8
    • 数据结构优化:JDK 1.8引入了红黑树结构,当链表长度过长时,将链表转换为红黑树,提高了查找效率。在高并发场景下,大量数据的查找操作性能得到显著提升。
    • 锁机制优化:摒弃分段锁,采用CAS + synchronized的方式,进一步降低了锁的粒度,提高了并发度。同时,synchronized在JDK 1.8中进行了优化,性能有了较大提升。
    • 扩容优化:JDK 1.8的扩容机制更加复杂和高效,允许并发扩容,减少了扩容时对系统性能的影响。在扩容过程中,部分线程可以继续进行读写操作,提高了系统的整体可用性。

综上所述,ConcurrentHashMap在高并发场景下通过不断优化数据结构和锁机制,提供了高效且线程安全的哈希表实现。