HashMap和ConcurrentHashMap总结思考

本文详细阐述了HashMap中key的hash值计算方法,涉及null值处理和数组下标计算策略,以及ConcurrentHashMap的底层实现,包括Node数组、红黑树和多线程安全的CAS技术。讨论了内存调优、线程同步和面试常见问题。

1.背景知识:
当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,在算出Hash值,然后计算数组元素下表的位置。

2.HashMap中如何计算hash值?如何计算数组元素下表位置?
要计算HashMap中对应的数组下标,必然要进行获取哈希值,HashMap对key的哈希值做了离散处理
对于HashMap中 key值的hash计算源码如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到 HashMap 中的key值可以为null,key值的hash值的计算方法为:key的hash值高16位不变,低16位与高16位异或作为key最终的hash值。(h>>>16,表示无符号右移16位,高位补0)

JDK 8:将原哈希值和左移16位的值,一起异或运算(^)。
下标计算:(put方法中摘抄)

n = tab.length
i = (n - 1) & hash

是再数组长度减一后,做与运算(&)。因为配合数组长度位2的次方数,所以相当于length-1取余

*为什么HashMap的容量是2的n次幂?

当 n 是2的次幂时, n - 1 通过 二进制表示即尾端一直都是以连续1的形式表示的。当(n - 1) 与 hash 做与运算时,会保留hash中 后 x 位的 1,这样就保证了索引值 不会超出数组长度。

这样做的好处在于:

&运算速度快,至少比%取模运算快

能保证索引值肯定在HashMap的容量大小范围内

(n - 1) & hash的值是均匀分布的,可以减少hash冲突
同时当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n。

3.HashMap中put进一个元素的过程?
1)先进行容量的判断,如果当前容量达到了阈值,先进性2倍扩容操作,重新计算hash值,和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。在1.8之前用头插法将Entry对象插入到链表的头部。
但是这种操作在高并发的环境下容易导致死锁,所以1.8之后,新插入的元素都放在了链表的尾部

2)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向3,这里的相同指的是hashCode以及equals;

3).判断table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向4;

4).遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

5).插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

ConcurrentHashMap:
以Node数组 或者 红黑树的方式存储对象,当存储的键值对出现冲突 ,且冲突的数量比较多时,用红黑树的方式能够提升数据读写的性能

1.ConcurrentHashMap底层存储元素时,Node数组存储节点,Node数组实现了Map.Entry<K,V>

static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;

    final K key;

    volatile V val;

    volatile Node<K,V> next;

通过volatile 修饰val 和next,为什么要使用volatile?

因为:多个线程会先把对象保存在本线程内部的内存中,随后再回写到主内存,这样操作的弊端是,当多线程并发时,就有可能引发数据问题
而变量一旦被 volatile 修饰,那么多个线程会直接读写主内存中的该变量,这样就避免了引发数据冲突

2.ConcurrentHashMap底层put进一个元素的过程?如何确保多线程安全性?

通过源码可以得知
put 方法 调用putVal方法实现了向 Node 数组或 TreeBin 红黑树中 添加数据。

1.首先如果Node数组为空 则调用init方法 初始化数组,同时计算hash值,并定位到Node中的位置, 如果Node 位置为空则没有冲突,则以 CAS 方式插入,多个线程尝试使用 CAS 同时更新同一个变量时, 只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败, 可以再次进行尝试
2.如果Node数组不为空,则对该 Node 加 synchronized 锁,并加入到该 Node 所指向的链表里, 如当前 Node 里包含的链表节点数大于 8,则用 treeifyBin 方法把链表转红黑树,

3.读一个元素的过程?
在读数据时,首先会计算 key 的 hash 值,如果 Node 数组里匹配到首结点即返回,否则调用 next 方法遍历链表或红黑树,由于可能会有冲突,因此需调用 equals 方法

底层实现多线程安全的原理机制:synchronized+CAS 的并发策略

三:面试常问的问题
1. 你了解volatile,Synchronized或CAS机制吗?

我知道,另外我还知道 ConcurrentHashMap 对象中用到这些

2.当问及任何HashMap或者集合的时候,我们引出来ConCurrentHashMap, 谈及并发,线程安全性。多线程写操作

讲到ConcurrentHashMap的底层, Node, 红黑树,volatile

ConcurrentHashMap 对象是根据哈希表结构存储键值对数据,以及用桶链路的方式处理冲突

加分调优项:

1)展示内存性能调优方面的技能
在项目上线前,我们做了压力测试,从中发现订单等模块的内存用量过大,通过观察日志,我们发现是由于 ConcurrentHashMap 对象保存的数据过多,用完以后没及时 clear 掉,由此引发了 OOM 问题。
因此当我们用好数据以后,应该立即 clear 掉该 ConcurrentHashMap 对象,内存性能就会好很多了。

2)展示多线程环境下数据读写方面的调优技能

在项目的数据同步模块中,需要把客户文件缓存到本地读写。一开始我们用的是 HashMap,但 HashMap 不是线程安全的,所以读写时都用了 Synchronized 关键字,这样处理的弊端是,当读写线程个数变多时,由于只有一个线程能读写,因此其他线程会阻塞。不仅性能慢,而且还会导致死锁。

后来我们改用 ConcurrentHashMap 对象,该对象对读操作不加锁,对写操作时会采用 synchronized+CAS 的并发策略,所以一定程度上提升了读写性能。

https://blog.youkuaiyun.com/q5706503/article/details/85171474

什么是CAS:

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, 
        new Node<K,V>(hash, key, value)))
        break;                   // no lock when adding to empty bin

}

CAS 的含义是 Compare And Swap,CAS(V,E,N),若 V 等于 E,则将 V 设为 N。若 V 和 E 不等,则说明已有其他线程做了更新,当前线程什么都不做,或更改 V、E 和 N 参数再重试。
https://blog.youkuaiyun.com/qq_41345773/article/details/92066554

HashCode背景知识

1.判断两个对象相等可以用hashcode比较吗?
回答是不可以。你必须用equals方法!两个不同对象可能hashcode相等,但两个不同hashcode的对象一定不同。
另外一点,如果覆写了equals方法,必须覆写hashcode方法,原因是默认的hashcode是将对象的存储地址进行映射。
而且逻辑上,如果两个对象的equals方法返回是相等的,那么它们的hashcode必须相等;反之不一定成立

2.什么是HashCode
Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,
这个数值称作为散列值。即在散列集合包括HashSet、HashMap以及HashTable里,
对每一个存储的桶元素都有一个唯一的"块编号",即它在集合里面的存储地址;当你调用contains方法的时候,
它会根据hashcode找到块的存储地址从而定位到该桶元素

设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。
如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,
那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,
hashCode()方法就会生成一个不同的散列码。

<think>嗯,用户询问的是ConcurrentHashMap在多线程下高效而HashMap在单线程下高效的原因。这是一个关于Java集合类性能差异的核心问题,我需要深入解析底层机制。 从引用的资料来看,有几个关键点值得注意:引用1明确指出单线程场景HashMap性能优于ConcurrentHashMap;引用3提到ConcurrentHashMap采用分段锁+CAS的设计理念;引用5则通过代码示例展示了ConcurrentHashMap在遍历时删除的安全性。这些正好构成回答的骨架。 先思考HashMap的优势点。在单线程环境下,它的高效主要源于两点:首先是完全无锁的数据结构,所有操作直接访问数组链表/红黑树,没有同步开销;其次是优化过的哈希算法扰动函数,碰撞概率低。这就像单人开车不需要考虑车道协调,可以全速行驶。 再分析ConcurrentHashMap的并发优势。引用3提到的分段锁设计很形象——类似高速公路的多车道并行。具体来说,在JDK7是通过Segment划分区块,不同线程操作不同区块时互不影响;到了JDK8则升级为更细粒度的桶级别锁+CAS+volatile组合。特别值得注意的是它在迭代器设计上的巧思:引用5的示例显示即使遍历时修改数据也不会抛出ConcurrentModificationException,这得益于它采用弱一致性的迭代器而非HashMap的快速失败机制。 不过用户可能没意识到的是,ConcurrentHashMap的性能优势仅在写竞争激烈时才明显。如果是纯读场景,由于它完全无锁的读实现(通过volatile保证可见性),性能几乎HashMap持平。这点值得在回答中强调。 还需要解释为什么ConcurrentHashMap在单线程时稍慢:每次操作要处理线程哈希、访问volatile变量、可能的CAS重试等。虽然JDK不断优化,但基础开销仍存在。就像检查站再高效也会让单车道的车流变慢。 最后要提醒实践要点:引用4给出的替换方案看似简单,但特别强调要验证所有依赖代码。这引出一个深层问题——线程安全是系统性工程,单纯替换集合类可能不够,需要整体考虑原子性操作等问题。 </think>ConcurrentHashMap在多线程下高效而HashMap在单线程下高效的原因主要源于**设计目标差异****同步机制开销**,具体分析如下: --- ### **一、HashMap在单线程下高效的原因** 1. **零同步开销** HashMap无任何同步控制(如`synchronized`或`CAS`),所有操作直接访问底层数组。单线程场景下避免了锁竞争、上下文切换等额外开销。 2. **优化的哈希算法** 通过扰动函数(如JDK8的`hash = key.hashCode() ^ (key.hashCode() >>> 16)`)降低哈希碰撞概率,链表/红黑树转换保证$O(1)$~$O(\log n)$的操作效率。 3. **内存访问局部性** 数据连续存储于数组,CPU缓存命中率高,适合单线程快速遍历(如`for`循环)。 > 💎 **本质**:牺牲线程安全性换取极致速度。 --- ### **二、ConcurrentHashMap在多线程下高效的原因** #### 🔧 **并发控制机制演进** | **版本** | **核心机制** | **优化点** | |----------|------------------------|-----------------------------------------| | JDK7 | **分段锁(Segment)** | 将数组分为16段,不同段可并发操作[^3] | | JDK8+ | **CAS + synchronized** | 桶粒度锁(锁单个链表/树节点),降低冲突 | #### 🚀 **高效关键设计** 1. **分段锁(JDK7)** 类比高速公路多车道:线程操作不同段(Segment)互不阻塞,并发度由段数决定(默认16)。 2. **CAS + 桶锁(JDK8)** - **读操作**:无锁访问(`volatile`变量保证可见性) - **写操作**: - 空桶插入 → CAS原子操作 - 非空桶 → 对桶头节点加`synchronized` ```java // JDK8 putVal() 核心逻辑(简化) if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) // CAS插入 break; } else { synchronized (f) { // 锁住桶头节点 // 链表/红黑树操作 } } ``` 3. **弱一致性迭代器** 迭代时允许并发修改,通过遍历原数组快照避免`ConcurrentModificationException`[^5],适合高并发遍历场景。 4. **扩容优化** 多线程协同扩容(线程迁移不同桶段),避免单点瓶颈。 > 💎 **本质**:最小化锁粒度 + 无锁读 + 并发扩容 → 高吞吐量。 --- ### **三、性能对比总结** | **场景** | HashMap | ConcurrentHashMap | |----------------|---------------------|----------------------------| | **单线程操作** | ⚡ 无锁,速度极快 | ⚠️ 有并发控制开销,略慢 | | **多线程操作** | ❌ 数据损坏风险 | ✅ 安全高效 | | **迭代修改** | ❌ 快速失败异常 | ✅ 弱一致性,安全遍历[^5] | --- ### 📚 **核心结论** 1. **单线程选HashMap** 无同步开销,性能碾压任何线程安全容器(包括ConcurrentHashMap)[^1]。 2. **多线程选ConcurrentHashMap** 分段锁/CAS+桶锁实现**并发度与容器大小解耦**,冲突越少性能越接近HashMap[^3][^4]。 3. **避免HashTable** 全表锁(`synchronized`方法级)导致并发度=1,性能最差。 > ⚠️ **误区警示**: > 即使使用ConcurrentHashMap,**复合操作**(如`map.putIfAbsent()`)仍需外部同步保证原子性! --- ### 相关问题 1. ConcurrentHashMap在JDK7JDK8中的实现有哪些本质区别? 2. 为什么ConcurrentHashMap的迭代器不会抛出ConcurrentModificationException? 3. 如何在保证线程安全的前提下进一步提升ConcurrentHashMap的写性能? 4. HashMap在多线程下可能引发哪些具体问题(如死循环)? 5. ConcurrentHashMap的`size()`方法为何需要特殊设计?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值