- 博主简介:努力的打工人一枚
- 博主主页:@xyk:
- 所属专栏: JavaEE初阶
Hashtable、ConcurrentHashMap是使用频率较高的数据结构,它们都是以key-value的形式来存储数据,且都实现了Map接口,日常开发中很多人对其二者之间的区别并没有十分清晰的概念。
目录
一、多线程环境使用哈希表
1.1什么是Hashtable?
1.2什么是ConcurrentHashMap?
二、ConcurrentHashMap的改进
三、ConcurrentHashMap和Hashtable的区别
3.1 加锁粒度的不同
3.2更充分的利用了CAS机制
3.3优化了扩容策略
四、相关面试题
4.1 ConcurrentHashMap的读是否要加锁,为什么?
4.2 介绍下 ConcurrentHashMap的锁分段技术?
4.3 ConcurrentHashMap在jdk1.8做了哪些优化?
4.4 Hashtable和HashMap、ConcurrentHashMap 之间的区别?
一、多线程环境使用哈希表
HashMap本身就不是线程安全的,那么多线程环境下使用哈希表可以使用:
- Hashtable
- ConcurrentHashMap
1.1什么是Hashtable?
本篇不再详细介绍,想要了解详细请点击:Hashtable是什么?它和Hashmap有什么区别?_xyk:的博客-CSDN博客
只是简单的把关键方法加上了synchronized关键字
加到方法上,相当于是针对this加锁了
- 如果多线程访问同一个Hashtable就会直接造成锁冲突
- size属性也是通过synchronized来控制同步的,速度也是很慢的
- 一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会非常低!!
1.2什么是ConcurrentHashMap?
ConcurrentHashMap底层是基于数组+链表,jdk1.7中的数据结构采用分段式设计,segment数组 + HashEntry数组 + 链表实现,hash冲突采用拉链法处理。
而在jdk1.8中,借鉴了jdk1.8中HashMap的设计思想,采用数组 + 链表 + 红黑树的数据结构,并且有原来的分段式锁换成了CAS + Synchronized锁,使用的是尾插法,其它的地方并没有改变。
二、ConcurrentHashMap的改进
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例;
- 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率
- 充分利用 CAS 特性,比如 size 属性通过 CAS 来更新,避免出现重量级锁的情况.
- 优化了扩容方式:化整为零
- ConcurrentHashMap不允许key或value为null值
三、ConcurrentHashMap和Hashtable的区别
3.1 加锁粒度的不同
Hashtable是针对整个哈希表加锁,任何的增删改查操作,都会触发加锁,也就可能会有锁竞争!!实际上仔细思考,其实没必要把锁加的这么勤快
插入元素:
根据key计算hash值 ->数组下标,把这个新的元素给挂到对应的下标的链表上,那么如果是俩个线程,插入俩个元素,虽然俩个操作没有线程安全问题,但是由于synchronized是加到this上,仍然会针对同一个对象产生锁竞争,产生阻塞等待!!!
那么如果是ConcurrentHashMap不是只有一把锁了,每个链表的头节点都作为一把锁~~
每次进行操作,都是针对对应链表的锁进行加锁,那么操作不同的链表就是针对不同的锁加锁,不会有锁冲突;
导致大部分加锁操作实际上没有所冲突的!!此时加锁操作的开销就微乎其微了~~~
上述内容是ConcurrentHashMap和Hashtable之间最大,最关键的,最核心的区别
伪码:
void put(String key,String value){
//先找到对应的链表的头节点
int index = hashCode(key);
Node head = getHead(index);
synchronized(head){
执行链表进行插入节点操作
}
}
3.2更充分的利用了CAS机制
更充分的利用了CAS机制——无锁编程;有的操作,比如获取/更新元素个数,就可以直接使用CAS完成,不必加锁了。CAS也能保证线程安全,往往比锁更高效。
3.3优化了扩容策略
对于Hashtable来说,如果元素太多了,就会涉及扩容;扩容需要重新申请内存空间,搬运元素(把元素从旧的哈希表上删掉,插入到新的哈希表上)如果本身元素非常多,上亿个,全部搬运一次,成本就很高,就会导致这一次put操作效率很低,非常卡顿!!
那么ConcurrentHashMap策略,化整为零;并不会试图一次性的就把所有元素都搬运过去,而是每次就搬运一部分;当put触发扩容,此时就会直接创建更大的内存空间,但是并不会直接把所有元素都搬运过去,而是只搬运一小部分,速度还是比较快的;
此时,相当于存在俩份hash表了,如果要插入元素,直接往新表拆入;删除元素,删除旧表,或元素在哪个表就删除哪里;查找,新表旧表都查找!!并且每次操作过程中,都搬运一小部分元素过去~~~
四、相关面试题
4.1 ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了
volatile 关键字
4.2 介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争
4.3 ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树
4.4 Hashtable和HashMap、ConcurrentHashMap 之间的区别?
- HashMap: 线程不安全. key 允许为 null
- Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null
- ConcurrentHashMap: 线程安全,使用 synchronized 锁每个链表头结点,锁冲突概率低, 充分利用CAS 机制,优化了扩容方式,key 不允许为 null