1、概述
HashMap:是 Java 中的集合类,实现了 Map 接口、可以存储 key-value 格式数据的数据结构。
HashMap 特点:
-
键值对存储:以 key-value 格式存储数据,key、value 可以是任何类型。
-
基于哈希表:使用哈希表来存储数据,通过 key 的哈希值来确定哈希表中位置。
-
非线程安全:在多线程环境中存在线程安全问题。
-
允许空:允许 key 和 value 为 null,但 key 只能有一个为 null。
-
动态扩容:当数量达到 capacity * loadFactor 时,会自动进行扩容。
-
键的唯一性:通过 key 的 hashCode() 和 equals() 来保证 key 的唯一。
HashMap 用途:
- 缓存:可用来存储频繁访问的数据,减少数据库查询或其他资源的访问次数。
- 计数器:可用于统计某元素出现次数。
- 数据去重:可利用 HashMap 中 key 的唯一性来实现数据去重。
- 配置管理:可用来存储和管理配置信息。
2、使用
2.1 构造方法
| 方法 | 说明 |
|---|---|
| HashMap() | 创建一个空的 HashMap。 |
| HashMap(int initialCapacity) | 创建一个初始容量为 initialCapacity 的 HashMap。 |
| HashMap(int initialCapacity, float loadFactor) | 创建一个初始容量为 initialCapacity、负载因子为 loadFactory 的 HashMap。 |
2.2 添加元素
| 方法 | 说明 |
|---|---|
| put(K key, V value) | 向 HashMap 中添加 key-value。 |
| putIfAbsent(K key, V value) | 在 HashMap 中不存在 key 时,向 HashMap 中添加 key-value。 |
| putAll(Map<? extends K, ? extends V> map) | 将 map 中的所有 key-value 添加到 HashMap 中。 |
2.3 获取元素
| 方法 | 说明 |
|---|---|
| get(K key) | 通过 key 获取对应的 value。 |
| getOrDefault(K key, V defaultValue) | 通过 key 获取对应的 value,若 key 不存在,则返回 defaultValue。 |
2.4 删除元素
| 方法 | 说明 |
|---|---|
| remove(K key) | 删除 key 对应的 key-value。 |
| clear() | 清除 HashMap 中所有 key-value。 |
2.5 检查元素
| 方法 | 说明 |
|---|---|
| containsKey(K key) | 检查 HashMap 中是否包含键为 key 的 key-value。 |
| containsValue(V value) | 检查 HashMap 中是否包含值为 value 的 key-value。 |
| isEmpty() | 检查 HashMap 是否为空。 |
2.6 获取信息
| 方法 | 说明 |
|---|---|
| size() | 获取 HashMap 中 key-value 的数量。 |
| loadFactor() | 获取 HashMap 的负载因子。 |
| capacity() | 获取 HashMap 的容量。 |
2.7 获取集合
| 方法 | 说明 |
|---|---|
| keySet() | 返回 HashMap 中所有 key 的 Set 集合。 |
| values() | 返回 HashMap 中所有 value 的 Collection 集合。 |
| entrySet() | 返回 HashMap 中所有 entry 的 Set 集合。(entry 包含一对 key-value) |
3、原理
3.1 JDK 7 实现
实现细节:
-
数据结构:数组 + 链表
-
哈希函数:key.hashCode()
3.2 JDK 8 实现
实现细节:
- 数据结构:数组 + 链表 + 红黑树
- 哈希函数:
4、相关集合
4.1 LinkedHashMap
LinkedHashMap:是 HashMap 的扩展,提供了一个链表来维护元素的插入顺序。
结构:
4.2 TreeMap
TreeMap:是 Java 中的集合类,实现了 SortedMap 接口,基于红黑树实现,可以保证元素的有序性。
结构:
4.3 ConcurrentHashMap
ConcurrentHashMap:是 Java 中 HashMap 的线程安全的实现。
1)JDK 7
结构图:
结构源码:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// 哈希表的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 哈希表的默认初始容量,需要平均分配给每个 Seqment
private static final int DEFAULT_CAPACITY = 16;
// 哈希表的默认并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 默认的加载因子,Seqment数组不可扩容,负载因子是给每个 Seqment 内部使用
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 哈希表的并发级别,默认 16,也就是默认 16 个 Seqment,支持 16 个线程并发写
private final int concurrencyLevel;
// 哈希表的容量
private final int segmentShift;
// 哈希表的段掩码,用于计算段索引
private final int segmentMask;
// 段的数量
private final int nsegments;
// 存储键值对的段数组
private final Segment<K,V>[] segments;
// 用于统计操作的序列号
private transient volatile long baseCount;
// 存储序列号的数组,用于统计操作
private transient volatile int[] counterCells;
// Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
// ...
}
// HashEntry
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
}
1)定位 Seqment
/**
* 计算 hash
*/
private static int hash(int h) {
h += (h << 15) ^ 0xffffcd7d; // 1. 左移 15 位与特定值异或 + h
h ^= (h >>> 10); // 2. 右移 10 位 异或 h
h += (h << 3); // 3. 左移 3 位,+ h
h ^= (h >>> 6); // 4. 右移 6 位 异或 h
h += (h << 2) + (h << 14); // 5. 左移 2 位 + 左移 14 位 + h
return h ^ (h >>> 16); // 6. 右移 16 位 + h
}
/**
* 根据 hash 确定 Seqment
*/
final Segment<K,V> segmentFor(int hash) {
return segment[(hash >>> segmentShift) & segmentMask];
}
2)get
public V get(Object key) {
int hash = hash(key.hashCode()); // 1. 计算 hash
return segmentFor(hash).get(key, hash); // 2. 定位 Segment,获取 value
}
3)put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 1. 尝试获取锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 2. 通过哈希值计算出在哈希表中的位置
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
K k;
if (e != null) {
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
HashEntry<K,V> next = e.next;
if (next == null) {
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;
}
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;
}
-
尝试获取锁,通过哈希值计算出在哈希表中的位置 。
-
插入:如果在该位置找到了现有的键值对(e != null),则检查键是否相同或哈希值和键相等。如果找到相同的键,则更新值(如果 onlyIfAbsent 为 false),并返回旧值。如果没有找到相同的键,且链表末尾没有节点,则在链表末尾插入新节点。
-
扩容:如果链表长度超过阈值(threshold),并且哈希表的容量小于最大容量(MAXIMUM_CAPACITY),则调用 rehash() 方法进行扩容。
-
如果不需要扩容,则直接在哈希表的对应位置插入新节点。
-
释放锁。
4)size
public int size() {
final Segment<K, V>[] segments = this.segments;
int size;
boolean overflow;
long sum;
long last = 0L;
int retries = -1;
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j) {
ensureSegment(j).lock();
}
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K, V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
-
使用 modCount,在 put()、remove()、clean() 等方法操作元素前,将 modCount + 1。
-
在计算 size 前后比较 modCount 是否发生变化。若未变化,返回计算得到的 size。若变化,重新统计计算。
2)JDK 8
结构图:
结构源码:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// 哈希表的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 哈希表的默认初始容量
private static final int DEFAULT_CAPACITY = 16;
// 哈希表的默认并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 默认的加载因子
private static final float LOAD_FACTOR = 0.75f;
// 当添加节点后链表的长度超过该数值时会将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当红黑树的节点个数小于该数值时,红黑树将转换回链表
static final int UNTREEIFY_THRESHOLD = 6;
// 当链表的长度大于8时,若哈希表的容量大于64,则将链表转换成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
// 扩容过程中每个线程负责的最小容量个数
private static final int MIN_TRANSFER_STRIDE = 16;
// 常量值,用于生成邮戳,标识当前线程正在扩容
private static int RESIZE_STAMP_BITS = 16;
// 常量值,限制帮助迁移哈希表节点的线程个数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 常量值,与邮戳进行计算来判断ConcurrentHashMap是否扩容完成
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 标识当前节点是ForwardingNode对象,即标识当前节点已经迁移到新哈希表中了
static final int MOVED = -1;
// 标识当前节点是红黑树,也就是当前节点使用TreeBin对象来包裹红黑树的根节点
static final int TREEBIN = -2;
// 标识ReservationNode对象,该对象主要用来上锁
static final int RESERVED = -3; // hash for transient reservations
// 通过与hash值 & 计算来保证hash值不会出现负数
static final int HASH_BITS = 0x7fffffff;
// 获取CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 哈希表
transient volatile Node<K,V>[] table;
// 新哈希表,当把旧哈希表的所有节点迁移到新哈希表后,那么就会把该值赋给table,最终在将该值给置null
private transient volatile Node<K,V>[] nextTable;
// 用于控制哈希表的初始化与扩容
private transient volatile int sizeCtl;
// 由于采用的从后面开始遍历,索引呈现递减,所以此属性可以说是剩余未迁移节点的数量/索引
private transient volatile int transferIndex;
// 通过标识来控制加锁(1)或释放锁(0),控制CounterCell数组
private transient volatile int cellsBusy;
// counterCells数组是LongAdder高性能实现的必杀器,当发生线程竞争的情况,会将该线程随机分配到某个索引上
private transient volatile CounterCell[] counterCells;
// 统计节点个数的机制
private transient volatile long baseCount;
// 标识当前节点是ForwardingNode对象
static final Node<K,V> MOVED = new Node<K,V>();
// Node
static final class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
}
1)spread():计算 hash 值
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
2)put():主要通过调用 putVal() 来实现元素添加。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 计算 hash 值
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();
// 寻找 hash 值对应的数组下标
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;
}
// hash == MOVED,表示正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 处理槽位上存在值的情况
else {
V oldVal = null;
// 锁住当前槽位
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表形式
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// key 已存在,判断是否需要进行覆盖
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;
// 到链表尾部也没有找到 key,将插到链表尾部
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;
}
3)get()
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算 hash 值
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 判断 头节点是否是需要的节点,如果是,则直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果头节点 hash < 0,说明是红黑树或正在扩容,使用 find() 进行查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// 如果整个数组是空,或者当前槽位的数据是空的,说明 key 对应的 value 不存在,直接返回 null
return null;
}