面试官: 你对ConcurrentHashMap了解多少?
我回答:
ConcurrentHashMap
在Java 8中进行了重大改进,旨在提高并发性能和减少锁竞争。在Java 8之前,ConcurrentHashMap
使用分段锁(Segment)来实现线程安全,每一段包含一个HashEntry
链表。然而,这种方式在多核处理器上并不高效,因为段的数量固定,且随着CPU核心数的增加,锁的粒度并没有减小。
Java 8中的ConcurrentHashMap
设计
在Java 8中,ConcurrentHashMap
放弃了分段锁的设计,转而使用细粒度的锁和CAS(Compare and Swap)操作来实现线程安全。以下是Java 8中ConcurrentHashMap
的主要特性:
-
锁分离:
ConcurrentHashMap
Java 8中的ConcurrentHashMap
放弃了Java 7中的Segment分段锁机制,转而采用更细粒度的锁策略。在Java 8中,锁被应用到了每个Node节点上,而不是整个数组或分段。这意味着多个线程可以同时访问和修改不同的Node节点,而不会相互干扰,从而大大提高了并发性能。
-
CAS和volatile:
- 使用CAS操作来更新元素,这使得在没有冲突的情况下,插入、更新或删除操作可以无锁完成。在执行写操作时,
ConcurrentHashMap
会首先尝试使用CAS操作来无锁地修改数据。如果CAS操作成功,则无需加锁即可完成数据的更新;如果CAS操作失败,则说明有其他线程正在修改该节点,此时会退而求其次,使用synchronized
锁来保证线程安全。 - 使用
volatile
关键字来确保可见性和有序性,这对于读取操作特别有用,因为读操作在没有写操作的情况下是完全无锁的。
- 使用CAS操作来更新元素,这使得在没有冲突的情况下,插入、更新或删除操作可以无锁完成。在执行写操作时,
-
数据结构: 数组+链表+红黑树:
- 初始存储结构是一个节点数组,每个位置上的节点可能是一个链表的头部(如果发生哈希冲突)。
- 当链表长度超过一定阈值(默认为8),数组长度大于64, 链表会转换成红黑树,以提高查找效率。
- 当树中的节点数减少到足够小时,红黑树会退化回链表。
-
动态扩容:
- 当
ConcurrentHashMap
的容量达到阈值时,它会自动进行扩容。扩容操作是线程安全的,并且可以并发进行,这减少了扩容带来的性能影响。
- 当
-
统计信息:
- Java 8的
ConcurrentHashMap
提供了一些新的方法,如size()
,isEmpty()
等,它们提供了近似的结果,因为在高并发环境下准确计算大小成本很高。
- Java 8的
实现细节
-
初始化:
ConcurrentHashMap
采用懒加载的方式初始化数组。当第一次向ConcurrentHashMap
中添加元素时,如果数组尚未初始化,则会通过initTable()
方法初始化一个长度为2的幂次方的数组。这个初始化过程是通过CAS(Compare-And-Swap)操作来确保线程安全的。在Java 8中使用Node<K,V>
作为基本的键值对容器,而在Java 9及以后版本中,引入了ForwardingNode
和TreeNode
等特殊类型的节点。
-
写操作:
- 写操作(如
put
)使用CAS或锁来确保线程安全。如果CAS失败(意味着有并发写操作),那么会使用锁。
- 写操作(如
-
读操作:
- 读操作通常是无锁的,因为
Node
节点的val
和next
字段都被声明为volatile
,这保证了内存可见性。因此,读操作可以直接访问这些字段而无需加锁。写操作则需要通过CAS或synchronized
锁来保证线程安全。
- 读操作通常是无锁的,因为
-
扩容过程:
- 当
ConcurrentHashMap
中的元素数量达到数组容量的某个比例(默认为0.75)时,会触发扩容操作。扩容操作会创建一个新的数组,并将旧数组中的元素重新分布到新的数组中。扩容是渐进式的,允许多个线程同时参与,每个线程负责转移数组的一部分。 - 扩容过程是通过
transfer()
方法实现的。在扩容时,线程会从数组的最后一个索引开始向前遍历,并将每个Node迁移到新的数组中。这种逆序迁移的策略可以减少线程间的冲突,从而提高扩容的性能。同时,扩容过程中还采用了多种机制来确保线程安全和数据一致性。
- 当
其他特性
1. 允许null值
与HashMap
不同,ConcurrentHashMap
允许键(Key)和值(Value)为null,但需要注意的是,如果键为null,则总是映射到数组的第一个位置。
2. 迭代器和分割器
ConcurrentHashMap
提供了弱一致性的视图迭代器(Iterator)和分割器(Spliterator),它们反映的是某一时间点或迭代开始时的集合状态,而不是实时的集合状态。
性能优势
Java 8中的ConcurrentHashMap
通过采用数组+链表+红黑树的数据结构、细粒度的锁策略、CAS操作以及高效的扩容机制,实现了高并发的读写操作。这种设计使得ConcurrentHashMap
在并发场景下具有较高的性能,并且减少了线程间的竞争和阻塞。因此,在需要高并发的Java应用程序中,ConcurrentHashMap
是一个非常重要的并发集合工具。