HashMap
HashMap是Java集合框架中的一个核心类,用于存储键值对。它实现了Map
接口,并基于哈希表(Hash Table)的数据结构来存储键值对。HashMap
允许键为null
,并允许多个键映射到同一个值。
数据结构
HashMap
底层的数据结构是一个数组,数组中的每个元素是一个链表或红黑树。这个数组称为哈希表(Hash Table),每个链表或红黑树称为桶(Bucket)。
每个键值对通过哈希函数计算键的哈希值,并将其映射到数组中的某个索引位置,从而决定存储位置。
在Java 8之前,HashMap使用链表处理冲突,但当链表长度过长时,性能会退化为O(n)。Java 8引入了红黑树,当链表长度超过阈值(默认是8)时,将链表转换为红黑树,从而提高查找和插入性能。
- 数组:哈希表是一个固定大小的数组,用于存储键值对。
- 链表:当两个或多个键通过哈希函数计算出的哈希值相同,即发生了哈希冲突时,这些键值对会被存储在同一个桶中的链表中。
- 红黑树:当链表中的元素数量超过一定阈值时,链表会被转换为红黑树,以提高查找效率。
实现原理
- 哈希函数:HashMap使用键的哈希码(hashCode()方法返回的值)计算键的哈希值。哈希值通过一个扰动函数(扰动函数的目的是减少哈希冲突)进一步处理,然后通过取模运算将其映射到数组的索引位置:
// 这里使用了一个位运算(h >>> 16),将哈希码的高位和低位混合,以减少冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 冲突处理:当两个键被映射到相同的数组索引时,称为哈希冲突。HashMap使用
链地址法
(separate chaining)处理冲突,即将相同索引位置的多个键值对存储在一个链表或红黑树中。 - 插入操作:插入一个键值对时,HashMap首先计算键的哈希值并找到对应的桶索引。如果桶为空,则直接将节点放入该桶。如果桶不为空,则沿链表或红黑树查找该键。如果找到相同的键,则更新值;否则,将新节点添加到链表或红黑树的末尾。
- 查找操作:查找操作根据键计算哈希值和索引,然后在链表或红黑树中查找该键。如果找到,则返回相应的值,否则返回null。
- 扩容机制:HashMap的
初始容量
和负载因子
(load factor)决定了何时进行扩容。默认初始容量为16,默认负载因子为0.75。当元素数量超过容量与负载因子的乘积时(即达到75%的负载因子),HashMap将进行扩容操作,通常是将容量加倍,并重新哈希所有现有元素。 - 优化:Java 8引入了红黑树,当链表长度超过一定阈值时,将其转换为红黑树,以提高性能。
ConcurrentHashMap
ConcurrentHashMap
是 Java 中的一种并发集合类,旨在提供高效的线程安全的哈希表实现。与 Collections.synchronizedMap
不同,ConcurrentHashMap
通过更细粒度的锁机制来实现高性能的并发访问。
数据结构
ConcurrentHashMap
的主要数据结构也是哈希表,但与 HashMap 不同,它采用了分段锁
(在 Java 8 之前)和CAS操作
(在 Java 8 及之后)来提高并发性能。
Java8之前的实现:
在 Java 8 之前,ConcurrentHashMap
通过使用多个独立的锁
(称为“段”,即 Segment)来实现并发控制。每个 Segment 类似于一个小的 Hashtable,具有自己的锁。这种设计使得多个线程可以并发地访问不同段上的数据,从而提高并发性能。
static class Segment<K,V> extends ReentrantLock {
// 内部维护一个哈希表
transient volatile HashEntry<K,V>[] table;
}
每个 Segment
内部维护一个哈希表,类似于 HashMap 的实现。通过将整个哈希表划分为多个段,ConcurrentHashMap 可以支持更高的并发度。
Java8之后的实现:
在 Java 8 之后,ConcurrentHashMap
进行了重大的重构和优化。它不再使用 Segment,而是直接在每个桶上使用细粒度的同步机制(CAS操作
和 synchronized 块
)。
主要的数据结构包括:
- Node类:
Node
是哈希表中的基本存储单元,类似于 HashMap 中的 Node。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
- 桶数组:
ConcurrentHashMap
直接使用一个 Node[] 数组作为哈希表的基础结构。
transient volatile Node<K,V>[] table;
实现原理
ConcurrentHashMap
的实现原理主要通过以下几个方面来保证高效的并发访问:
- 分段锁(Java8之前):使用
Segment
来分段锁定,每个 Segment 内部是一个独立的哈希表,具有自己的锁。这样可以允许多个线程同时访问不同的段,从而提高并发性能。 - 细粒度锁(Java 8 之后):在 Java 8 之后,通过在每个桶上使用细粒度的锁(如
CAS 操作和 synchronized 块
)来实现更高效的并发控制。 - 冲突处理:通过链表和红黑树处理冲突,当链表长度超过阈值时转换为红黑树。
- 扩容机制:当负载因子超过阈值时进行扩容,通过重新哈希所有元素并重新分布到新的桶中来实现。
ConcurrentHashMap 和HashMap 对比分析
线程安全性
- ConcurrentHashMap:是线程安全的,允许多个线程并发访问而不需要额外的同步措施。它通过分段锁或细粒度锁来实现高并发。
- HashMap:是非线程安全的,如果多个线程尝试修HashMap,可能会产生竞争条件,导致数据不一致。
性能
- ConcurrentHashMap:在高并发环境下,由于其线程安全的特性,通常具有更好的性能。它允许多个线程同时读写,减少了锁竞争。
- HashMap:在单线程环境下,由于没有线程安全的要求,通常比ConcurrentHashMap有更高的性能。
锁策略
- ConcurrentHashMap(Java 8之前):使用分段锁,将数据分成多个段,每个段独立加锁。
- ConcurrentHashMap(Java 8及之后):使用更细粒度的锁,每个节点可以独立加锁,提高了并发性能。
- HashMap:不需要锁,但在多线程环境下,需要外部同步。
内部数据结构
- ConcurrentHashMap:使用数组、链表和红黑树(Java 8及之后)来存储数据。当链表长度超过阈值时,链表会转换为红黑树。
- HashMap:使用数组和链表来存储数据。当链表长度超过阈值时,会进行扩容操作。
内存占用
- ConcurrentHashMap:由于需要存储锁信息或维护更复杂的数据结构,通常比HashMap占用更多的内存。
- HashMap:内存占用相对较小,因为它不需要额外的线程安全措施。
迭代器
- ConcurrentHashMap:提供了弱一致性迭代器,允许在迭代过程中看到部分修改,但不会抛出ConcurrentModificationException。
- HashMap:迭代器是强一致性的,如果检测到并发修改,会抛出ConcurrentModificationException。
适用场景
- ConcurrentHashMap:适用于需要高并发访问和修改的场景,如多线程应用程序中的共享数据存储。
- HashMap:适用于单线程环境或在多线程环境中由外部同步控制访问的场景。
扩展性
- ConcurrentHashMap:设计时就考虑了并发,因此更容易扩展到多线程环境。
- HashMap:在多线程环境下需要额外的同步措施,这可能会限制其扩展性。