目录
5.1.为什么重写equals方法需同时重写hashCode方法?
5.2.HashMap 的Java 1.8版本相对于1.7版本进行了哪些优化?
Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。而HashMap是用哈希算法实现Map的具体实现类
一、HashMap
HashMap是基于哈希表的Map接口的非同步实现,继承自AbstractMap,AbstractMap是部分实现Map接口的抽象类。
并且允许使用null键和null值,因为key不允许重复,所以只能有一个就键为null,另外HashMap不能保证放入元素的顺序,所以他是无序的,和放入的顺序并不能完全相同。而且HashMap是线程不安全的(主要体现在Put方法)。
二、哈希冲突
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?
也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。
前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
在之前的版本中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当链表中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap数据结构图:
三、HashMap的基本属性及构造
3.1 基本属性
/**
* 默认初始化大小 16(位运算 1<<4 --> 2^4=16)
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* HashMap最大容量 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子 当负载等于容量的0.75时,需要进行扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表的最大长度,即需要转换红黑树的边界,当链表长度达到8的时候就需要将链表转换成树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 在哈希表扩容时,如果发现链表长度小于 6,则会由树重新退化为链表。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
为什么负载因子为0.75呢?
通过大量实验统计得出来的,如果过小,比如0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如1,那么当元素满的时候才进行扩容,会使get,put操作的碰撞几率增加。
同时hashmap不是无限增大容量的,当达到极限的时候就不再进行扩容:MAXIMUM_CAPACITY
扩容机制
扩容的步骤如下:
🔸 (1) 计算新容量
-
HashMap
新容量 = 旧容量 × 2,即 翻倍增长。 -
例如:16 → 32 → 64 → 128...
-
threshold
也会随之变化,threshold = newCapacity * loadFactor
。
🔸 (2) 创建新的数组
-
创建一个新的哈希桶数组,大小是
newCapacity
。
🔸 (3) 重新计算哈希位置(rehash)
-
HashMap
需要重新分配所有节点的位置,因为扩容后数组长度变了,元素的存储索引(hash & (length - 1))
也会变。 -
Java 8 的优化:如果
hash & oldCapacity == 0
,则节点位置不变,否则移动到新索引oldIndex + oldCapacity
。
🔸 (4) 迁移元素
-
逐个遍历旧数组中的元素,将其重新分配到新数组的合适位置。
-
由于数组大小是 2 的幂,Java 8 通过
hash & oldCapacity
判断元素是否需要移动,提高了效率。
在 Java 7 和 Java 8 中,HashMap
的扩容方式有所不同:
版本 | 扩容方式 |
---|---|
Java 7 | 重新计算哈希值并重新插入新数组(rehash,可能导致链表顺序变化) |
Java 8 | 采用 hash & oldCapacity 方式判断元素是否迁移,减少 rehash |
Java 8 迁移优化
在 Java 8 中,HashMap
不会对所有元素重新计算哈希值,而是通过 hash & oldCapacity
来判断元素是否移动:
- 若
hash & oldCapacity == 0
,元素 留在原索引。 - 若
hash & oldCapacity != 0
,元素 移动到新索引 = 原索引 + oldCapacity。
这种方式减少了 hashCode
计算次数,提升了扩容效率。
3.2 HashMap.Node<K, V>
HashMap.Node<K, V>
是 HashMap
内部的一个静态类,它是哈希表的基本存储单元,表示存放在哈希桶(bucket)中的一个键值对(entry)。每个 Node
代表一个键值对,并且支持链地址法(拉链法)处理哈希冲突。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//hash存储当前键的哈希值,加快查找效率,避免重复计算key.hashCode()。
final K key;//存储键,final 修饰,防止修改。
V value;//存储值,可以修改。
Node<K,V> next;//指向链表的下一个节点,实现拉链法(链地址法)处理哈希冲突。
//Node用于创建 Node 实例,并将 next 作为链表的下一个节点。如果发生哈希冲突,多个 Node 以链表的方式链接在一起。
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//获取键值对的 键 和 值,是 Map.Entry 接口的方法。
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//计算 Node 的哈希值,^ 是异或运算,避免 key 和 value 哈希碰撞。
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//更新 value 并返回旧值。
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个 Node 是否相等,要求 key 和 value 都相等。
public final boolean equals(Object o) {
if (o == this)
return true;
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue());
}
}
3.3 HashMap的构造器
HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值,initialCapacity默认为16,loadFactory默认为0.75
1.HashMap(int initialCapacity, float loadFactor)//指定 初始容量 和 负载因子(扩容阈值)。
2.HashMap(int initialCapacity)//只指定初始容量,负载因子默认
0.75
。3.
HashMap()//
默认构造器,初始容量16
,负载因子0.75
。4.public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
直接使用已有
Map
作为数据源,创建一个新的HashMap
并拷贝其数据。容量会根据
m.size()
计算,确保容器大小合理,减少扩容开销。
putMapEntries(m, false)
会遍历m
并插入HashMap
,但不会触发扩容。
一般我们就是 new HashMap<>()直接使用了。
四、HashMap的相对应操作
4.1 HashMap().put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
那么直接看putVal,我加了注释
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 判断哈希表 table 是否为空,若为空则调用 resize() 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算存储索引 i,如果该位置为空,直接插入新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 3. 该位置已有节点,进行冲突处理,说明新的数据和旧的数据哈希冲突,即它们的哈希索引 i = (n - 1) & hash 计算结果相同,
//导致它们存储在 HashMap 的同一个桶(bucket)中。
Node<K,V> e; K k;
// 3.1 检查当前位置的第一个节点(桶头)是否与新插入的 key 相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 找到相同 key 的节点,记录到 e
// 3.2 该节点是树节点,调用 `putTreeVal` 进行红黑树插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 3.3 该节点是链表,遍历链表查找是否已存在 key
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 找到链表尾部
p.next = newNode(hash, key, value, null); // 在尾部插入新节点
if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度超过阈值,转为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 找到相同 key,退出循环
p = e; // 继续向后遍历
}
}
// 4. 如果 key 已存在,更新 value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent = true 表示仅在 key 不存在时插入
e.value = value;
afterNodeAccess(e); // 允许子类扩展,LinkedHashMap 复写该方法
return oldValue;
}
}
// 5. 结构修改计数 +1
++modCount;
// 6. 插入后,若 size 超过 threshold,触发扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 允许子类扩展
return null;
}
核心逻辑解析
-
检查 HashMap 是否初始化。如果
table
为空,调用resize()
进行初始化(默认容量16
)。 -
计算索引并插入,计算
i = (n - 1) & hash
确定数组索引位置。如果该位置为空,直接插入newNode()
。 -
处理哈希冲突
如果索引位置已有节点:- 如果是链表,遍历链表:
- 找到相同 key,则更新 value。
- 否则,在链表尾部插入新节点。
- 如果链表长度超过
TREEIFY_THRESHOLD=8
,转换为红黑树。
- 如果是
TreeNode
(红黑树),调用putTreeVal()
处理。 - 如果 key 相同,直接更新 value。
- 如果是链表,遍历链表:
-
更新 HashMap 结构
modCount++
记录修改次数(用于fail-fast
)。- 如果
size
超过threshold
,调用resize()
扩容。
-
调用钩子方法
afterNodeAccess(e)
: 允许LinkedHashMap
复写该方法,用于维护访问顺序。afterNodeInsertion(evict)
: 允许LinkedHashMap
复写该方法,可能用于 LRU 逻辑。
扩展知识
-
(n - 1) & hash
计算索引,n - 1
保证索引在[0, n-1]
范围内,& 运算代替%
运算,优化性能。 -
链表转红黑树,
TREEIFY_THRESHOLD = 8
,链表长度 ≥ 8 时,转换为红黑树,提高查找效率。 -
为什么
resize()
是 2 倍扩容?HashMap 使用 2^n 容量,扩容后(n - 1) & hash
保持哈希分布均匀。二进制,是吧! -
哈希冲突,有两种情况:
-
完全相同的 key(哈希值相同,key 也相同)
- 这意味着新数据的
key
和旧数据的key
是相同的。 - 此时
putVal()
方法会更新 value,而不是新增节点。
- 这意味着新数据的
-
不同的 key 但哈希值相同或索引相同(哈希冲突)
- 由于 HashMap 采用
(n - 1) & hash
计算索引,不同的 key 可能映射到同一个索引位置。 - 此时,HashMap 采用链地址法(链表或红黑树)来存储冲突的元素:
- 链表:如果冲突的节点较少(< 8 个),采用链表存储。
- 红黑树:如果冲突的节点 ≥ 8 个,转换为红黑树存储,提高查找效率。
- 由于 HashMap 采用
-
4.2 HashMap().remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 移除指定 key 对应的节点(Node)。
*
* @param hash key 的 hash 值
* @param key 要删除的 key
* @param value 可选:如果 matchValue 为 true,则仅在值匹配时删除
* @param matchValue 是否匹配 value,true 表示 key-value 必须匹配才能删除
* @param movable 是否允许调整数据结构(主要用于红黑树)
* @return 被移除的节点(如果存在),否则返回 null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; // 哈希表(数组)
Node<K,V> p; // 当前遍历的节点
int n, index; // 数组长度 & 计算出的索引
// 1. 确保哈希表非空,并找到 key 可能所在的桶(bucket)
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) { // 计算索引 index = (n - 1) & hash
Node<K,V> node = null, e; // `node` 代表要删除的节点,e只是个辅助变量,最开始为null,到后面才赋值
K k;
V v;
// 2. 检查桶的头结点是否匹配(hash 值相同且 key 相等)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 3. 如果头结点不匹配,遍历后续节点(链表 或 红黑树)
else if ((e = p.next) != null) {
if (p instanceof TreeNode) // 3.1 如果是红黑树,使用树的方式查找节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { // 3.2 普通链表,逐个查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 找到要删除的节点
break;
}
p = e; // 继续遍历
} while ((e = e.next) != null);
}
}
// 4. 如果找到了 key 对应的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 4.1 红黑树节点,调用树的删除方法
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 4.2 链表头结点,直接更新 table[index]
tab[index] = node.next;
else // 4.3 链表中的普通节点,修改前驱节点的 next 指针
p.next = node.next;
++modCount; // 结构发生变化,修改计数
--size; // 元素数量减少
afterNodeRemoval(node); // 触发子类的扩展方法
return node; // 返回删除的节点
}
}
return null; // 没找到,返回 null
}
-
哈希寻址:
- 计算索引:
index = (n - 1) & hash
,找到 key 可能所在的桶。 - 先检查头结点是否匹配,再遍历链表或红黑树查找。
- 计算索引:
-
删除节点:
- 如果是红黑树节点,调用
removeTreeNode()
。 - 如果是链表的头结点,直接修改
table[index]
。 - 如果是链表的中间节点,调整前驱节点的
next
指针。
- 如果是红黑树节点,调用
-
维护
HashMap
结构:modCount++
:保证并发修改时 fail-fast 机制生效。size--
:元素数量减少。afterNodeRemoval()
:提供给子类扩展,如LinkedHashMap
可用于维护访问顺序。
4.3 HasmMap()的get方法
public V get(Object key) {
Node<K, V> e;
// 调用 getNode 方法查找键对应的节点,如果找到则返回其值,否则返回 null
return (e = getNode(key)) == null ? null : e.value;
}
/**
* 获取指定 key 对应的节点。
* 该方法实现了 Map.get 及相关方法的逻辑。
*
* @param key 要查找的键
* @return 对应的节点,如果不存在则返回 null
*/
final Node<K, V> getNode(Object key) {
Node<K, V>[] tab; // 哈希表数组
Node<K, V> first, e; // first:桶中的第一个节点,e:用于遍历链表的辅助变量
int n, hash; // n:哈希表长度,hash:键的哈希值
K k; // 用于存储节点的键
// 检查哈希表是否为空,并获取哈希表长度
if ((tab = table) != null && (n = tab.length) > 0 &&
// 计算 key 的哈希值,并定位到对应的桶
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
// 如果桶中的第一个节点的哈希值和 key 相同,并且 key 也相等,则直接返回该节点
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果该桶存在链表或红黑树结构
if ((e = first.next) != null) {
// 如果是红黑树结构,则调用红黑树的 getTreeNode 方法查找
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
// 遍历链表查找匹配的节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 如果未找到对应的节点,则返回 null
return null;
}
五、十万个为什么
5.1.为什么重写equals方法需同时重写hashCode方法?
1. HashMap 的存储规则
HashMap 依赖 hashCode + equals 进行 key 的存储和查找:
-
计算
key
的hashCode
,确定存储的桶索引。 -
如果该桶已有元素:
-
遍历桶内的链表(或红黑树),调用
equals()
逐个比较 key。 -
若
equals()
返回true
,则认为 key 相同,更新value
。
-
2. 如果只重写 equals()
,会导致 HashMap 失效
HashMap 依赖 hashCode()
先定位 key 的存储位置,若 hashCode()
计算错误,可能会导致:
-
同一个 key 被存储到不同的 bucket,导致查找失败。
-
HashMap 认为两个相等的 key 是不同的 key,导致数据重复存储。
例子
class Person {
String name;
Person(String name) {
this.name = name;
}
//@Override 注释掉
// public int hashCode() {
// return name.hashCode(); // 让 hashCode 一致
// }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return this.name.equals(other.name);
}
}
public class Main {
public static void main(String[] args) {
HashMap<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
map.put(p1, "Person 1");
System.out.println(map.get(p2)); // 可能返回 null
}
}
记住,所有基于哈希的数据结构(如
HashMap
、HashSet
、Hashtable
、ConcurrentHashMap
)都需要同时重写equals()
和hashCode()
,否则可能会导致存储和查找异常。但对于 TreeSet、TreeMap、ArrayList、LinkedList 等非哈希结构的集合,就 不要求 重写hashCode()
,但可能需要重写equals()
和compareTo()
(如果它们基于排序)。
5.2.HashMap 的Java 1.8版本相对于1.7版本进行了哪些优化?
1.数组+链表改成了数组+链表或红黑树
-
HashMap 采用 数组 + 链表 结构时,在哈希冲突较多的情况下,查询效率可能退化为 O(n)。从 JDK 8 开始,当链表长度超过 8(阈值)时,自动转换为 红黑树,使查询效率提升为 O(log n)。
-
插入性能优化,传统链表插入是 O(1),而红黑树插入是 O(log n),因此:在链表过长时,转换为 红黑树,提高查询性能。在链表较短时,使用 链表,保持高效插入
-
删除效率提升,链表 删除元素需要遍历,最坏 O(n)。红黑树 删除元素 O(log n),提升删除性能。
2.头插法变尾插法
链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;尾插法避免了闭环的发生。因为在 多线程环境 下,多个线程同时扩容 HashMap
时,由于头插法会反转链表的顺序,可能会导致 环形链表,进而引发 死循环。
3.扩容机制判断更改
在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小。(因为二进制,&运算和扩容是2倍数)
3.HashMap的线程安全
首先是HashMap,他是线程不安全的。在多线程环境下,JDK 1.7 会产生死循环(闭环)、数据丢失、数据覆盖的问题,JDK 1.8 中会有数据覆盖的问题。
如何实现线程安全的Map呢? HashTable和ConcurrentHashMap
主要区别对比
特性 |
|
|
|
---|---|---|---|
线程安全 | ❌ 非线程安全 | ✅ 线程安全(使用 | ✅ 线程安全(使用分段锁) |
性能 | ⭐⭐⭐⭐⭐ 最快(无锁) | ⭐⭐ 最慢(全局锁) | ⭐⭐⭐⭐ 高效(局部锁) |
锁机制 | ❌ 无 |
| volatile+CAS + 分段锁(局部锁,提高并发性能) |
数据结构 | 数组 + 链表 + 红黑树(JDK 8+) | 数组 + 链表 | 数组 + 链表 + 红黑树 |
允许 | ✅ 允许 | 都不允许 | 都不允许 |
适用场景 | 单线程,非线程安全 | 多线程(但性能差) | 高并发,多线程场景 |
JDK 版本 | JDK 1.2 | JDK 1.0(过时,不推荐) | JDK 1.5+ |
5.4.LinkedHashMap
LinkedHashMap
继承自 HashMap
,在 HashMap
的**哈希表(数组 + 链表/红黑树)**基础上,额外维护了一个双向链表,用于记录访问顺序或插入顺序。
LinkedHashMap
继承了 HashMap.Node
,并新增了 before
和 after
指针,形成双向链表:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 指向前后节点的指针(双向链表)
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
因为它们存储了对其他节点的引用,用于链接 LinkedHashMap
中的节点,使其形成双向链表。
在 Java 中,尽管没有指针的概念,但对象引用(Reference) 在本质上起到了类似指针的作用。
5.5.ConcurrentHashMap的锁更新
🔹 1. 分段锁(Segment Lock)
分段锁是 ConcurrentHashMap
中的一种实现机制,最早出现在 Java 5 版本的实现中(在 Java 8 中,分段锁被优化为更细粒度的锁,即桶锁)。
工作原理:
-
分段(Segment) 是一个
HashMap
的子结构,每个分段是一个独立的HashMap
实例,并且每个分段都有自己独立的锁。(所以内存开销较大) -
每个分段 存储一部分数据,每个分段的大小通常与总容量成比例。
-
操作流程:
-
当一个线程访问某个元素时,首先会计算出该元素所在的分段。
-
然后,线程只会锁住对应的分段,而不是整个
ConcurrentHashMap
,从而避免多个线程对整个HashMap
进行互斥。 -
这样就实现了在多个线程并发操作时的并行性。
-
优点:
-
减少锁竞争:多个线程可以并发地访问不同的分段,减少了锁竞争。
-
提高并发性:通过分段锁机制,
ConcurrentHashMap
可以在多核处理器上提供更高的并发性能。
缺点:
-
分段锁导致内存开销:每个分段都需要额外的存储空间,这会导致内存使用的开销。
-
实现复杂性:分段锁的实现相对复杂,需要管理多个分段和各自的锁。
2. 桶锁(Bucket Lock)
在 Java 8 中,ConcurrentHashMap
采用了更细粒度的锁,称为 桶锁。这种方式实际上将分段锁替换为对单个桶的锁。
工作原理:
-
ConcurrentHashMap
的底层是一个数组,其中的每个元素称为 桶,每个桶用于存储多个键值对,采用链表或者红黑树来组织。 -
每个桶 都有一个独立的锁,线程在访问某个桶时,只会锁定该桶而不是整个
ConcurrentHashMap
。 -
桶的数量 与容量成比例,每个桶都独立存储不同的哈希值对应的数据。
-
并发读写:多个线程可以并发访问不同的桶,同时锁定不同的桶,避免了大量线程竞争一个锁。
优点:
-
更细粒度的锁:通过对桶的单独加锁,
ConcurrentHashMap
实现了比分段锁更细粒度的并发控制。 -
提高性能:减少了竞争的范围,尤其是对于大容量的
ConcurrentHashMap
,不同线程访问不同的桶时,几乎没有锁竞争,性能更优。 -
节省内存:相比分段锁,桶锁的实现内存开销较小,因为每个桶只需要锁,而不是整个分段。
缺点:
-
锁粒度过细:对于一些非常小的映射,桶锁可能会导致资源过度分配,尤其是在桶数较多的情况下,内存可能会浪费。
相关文章: