📚 目录
- 1. 全方位对比表格
- 1.1 线程安全与锁机制
- 1.2 null‑value规则
- 1.3 默认容量、扩容、底层数据结构
- 2. 底层细节区分
- 2.1 HashTable为什么现在不推荐使用?
- 2.2 ConcurrentHashMap相较于HashTable优势
- 3. 面试高频问答题库
前言:
本篇文章专注剖析 HashMap、HashTable、ConcurrentHashMap 的底层差异、版本迭代细节、并发坑点以及面试考点,适合用来复盘Java并发集合知识。
1. 全方位对比表格
1.1 线程安全与锁机制
| 容器 | 线程安全性 | 上锁原理 |
|---|---|---|
| HashMap | 非线程安全 | 没有添加任何锁;多线程并发编程中写入会产生数据覆盖、环形链表等问题 |
| HashTable | 线程安全 | 所有读和写的方法添加了synchronized,锁住整张哈希表对象,全局独占锁,并发的性能较差 |
| ConcurrentHashMap | 线程安全 | 采用桶头结点锁+CAS的方式;仅锁住当前正在操作的数组桶,其余桶可以多线程同时读写 |
1.2 null‑value规则
- HashMap: 允许使用1个null来作为主键,可以存放多个null值
- HashTable: K,V两个值都禁止使用null值,否则编译器会抛出
NullPointerException异常
例如:
3. ConcurrentHashMap: 同样K,V值不支持使用null,否则抛出NullPointerException异常
1.3 默认容量、扩容、底层数据结构
- HashMap
- 初始容量为
16, - 负载因子
0.75, - 扩容方式采用
2倍扩容 - 当满足条件时,链表会转化成红黑树
- HashTable
- 初始容量:
11 - 负载因子:
0.75 - 扩容规则:
newCapacity = oldCapacity * 2 + 1 - 不会转化成红黑树
- ConcurrentHashMap
- 初始容量:
16 - 负载因子:
0.75 - JDK8中依旧只有数组和链表的形式,不会转化成红黑树JDK8以后,ConcurrentHashMap具备转化成红黑树的能力;
2. 底层细节区分
2.1 HashTable为什么现在不推荐使用?
上诉谈到:HashTable是线程安全的,但是在实际开发中并不怎么使用HashTable,理由如下:
假设:我们两个以及以上的线程需要修改很多次HashTable中的数据;
当很多个线程在没有锁的情况下去修改同一个数据的时候,会触发线程安全问题;
此时很多个线程同时操作当前哈希表中相同一个链表的数据,由于有锁此时不会发生线程安全问题;
但是此时如果很多条线程操作不同链表中的数据,此时这把锁还需要上吗?
我们知道,就算此时就算不加锁,也不会触发线程安全问题,如果链表中有很多数据,另外两条线程拿不到锁就会一直处于阻塞状态,不会执行,就会减少执行的效率
2.2 ConcurrentHashMap相较于HashTable优势
相较于:HashTable,ConcurrentHashMap大幅度提升了执行效率:
- JDK7:Segment分段锁
ConcurrentHashMap采用了给哈希表的每一个节点进行上锁,每一把锁对应不同的链表
当不同线程去访问不同链表时,不会进行阻塞,访问同一条链表时,就会进行阻塞等待; - JDK8:彻底废弃Segment
改用Node桶节点锁和CAS自旋。当准备修改桶内数据,仅锁住链表头部节点。(CAS自旋,compare and swap,比较并交换,CAS也属于解决线程安全的一种手段,此处不具体讲解CAS);
好处:锁粒度细化到每一个哈希桶,并发吞吐量大幅度提升。
3. 面试高频问答题库
- HashMap 在多线程环境为什么不安全?
JDK7头插法扩容容易产生环形链表,然后就会触发死循环;多个线程同时执行put操作时,会发生value值覆盖丢失。 JDK8改成尾插解决环形链表,但依旧存在并发写入覆盖问题。
- ConcurrentHashMap 相较于 HashTable 的性能优势?
HashTable 锁住是整张哈希数组;ConcurrentHashMap 只锁定当前操作的桶结点,其余位置可以并发读写,冲突概率很低,高并发场景性能远超 HashTable。
- 为什么 ConcurrentHashMap 的 value 不能等于 null?
并发读取的时候,get (key) 返回 null,你没办法区分是该 key 不存在,还是 key 对应的存储值就是 null。单线程可以二次调用 containsKey 判断,并发情况下两次调用之间数据随时会被别的线程改动,判断失效。