在多线程并发执行的环境下使用哈希表,HashMap 本身是线程不安全的、Hashtable 虽然是线程安全的,但是相比ConcurrentHashMap 这个同样线程安全的哈希表来说效率太低。
一. 多线程环境中的哈希表
1. Hashtable
Hashtable 保证线程安全的办法就是简单的给各种关键方法都加 synchronized 关键字,相当于直接针对Hashtable 引用对象本身加锁。
Hashtable 保证线程安全相当于通过synchronized 进行锁表操作,这就使得多线程访问同一个Hashtable 就会直接造成锁冲突。同时计算哈希表中元素个数 (size)的属性也是通过加锁来控制的,这就使得锁冲突会比较激烈。
这样的锁表操作使得多线程访问不同桶下的元素都会因为竞争锁而阻塞等待。
2. ConcurrentHashMap[★★★]
CoucurrentHashMap 是在保证线程安全的前提下,对Hashtable 的上位替代。ConcurrentHashMap 不是通过对整个哈希表加一个全局锁来保证线程安全的,而是按照同级别进行加锁,从而降低锁冲突的概率,进而提高效率。
ConcurrentHashMap 中每个哈希桶都有一把锁,只有两个线程访问的恰好为同一个哈希桶上的数据才会出现锁冲突。
ConcurrentHashMap 相较Hashtable 做出的优化:
- 读操作没有加锁(但是使⽤了
volatile
保证从内存读取结果),只对写操作进⾏加锁。加锁的方式仍然是是用synchronized
,但是不是锁表 (哈希表的引用对象),⽽是通过锁桶 (将每个链表的头结点作为锁对象) 从而降低锁冲突的概率。- 利用CAS机制 来实现
size
属性的更新,(即原子类来维护size),避免加锁解锁的开销。- 当触发扩容操作时,采用化整为零的思想,而不是“一口气”完成扩容。
- 化整为零这个过程主要就是将整个扩容过程,拆分成多次来完成,触发扩容操作后续的每次操作ConcurrentHashMap的线程都会负责一部分扩容操作。
- 扩容会创建一个新的数组,整个扩容期间新老数组会同时存在,直到搬完最后一个元素才会将老数组删掉。
- 扩容期间,插入元素只往新数组中加。查找和删除元素则需要同时操作新老数组。
相关面试题:
1. ConcurrentHashMap的读是否要加锁,为什么?
答:读操作没有加锁,而是通过volatile 关键字保证从内存读取结果的正确性,从而进一步降低锁冲突。
2. ConcurrentHashMap在(jdk1.8)做了哪些优化?
答:取消了分段锁,直接给每个哈希桶(每个链表)分配了⼀个锁(就是以每个链表的头结点对象作为锁对象) -- “锁桶”。
# 分段锁:是Java1.7中采取的技术。Java1.8中已经不再使⽤了.简单的说就是把若⼲个哈希桶分成⼀个"段"(Segment),针对每个段分别加锁。
3. Hashtable 和 HashMap 、ConcurrentHashMap 之间的区别?
HashMap:线程不安全,key值允许为null。
Hashtable:线程安全,使⽤synchronized 锁Hashtable 对象 (锁表),key 值不允许为null。
ConcurrentHashMap:线程安全,使⽤synchronized 锁桶,锁冲突概率低,利用CAS 机制维护size ,优化扩容方式,key 值不允许为null。