面试必备:ConcurrentHashMap及其相关

128 阅读3分钟

面试必备:ConcurrentHashMap及其相关

为何使用ConcurrentHashMap?

说到ConcurrentHashMap就不得不提的同类产品:hashmap和hashtable

1. HashMap的线程安全问题

HashMap在单线程的情况下确实有很高的效率,但不支持并发操作,没有同步方法,在jdk1.8之前,Hash在并发执行Put操作时会发生死循环的问题。这涉及到hashmap的扩容过程。让我们回顾下hashmap的扩容。

当HashMap的负载因子(填入表中的元素个数/散列表的长度)达到一个数值时,散列表的key值映射冲突几率会逐渐提高,为提高平均查找效率,HashMap就会扩展它的HashMap长度。也就是扩容机制。

HashMap默认设定的装载因子为0.75(可改),HashMap的大小为length,已经装载的元素数量为num,当( num / length )> 装载因子时, 开始扩容,扩容过程如下:

  1. 先申请一个空间为旧列表两倍大的空间
  2. 将旧列表的节点以头插法(jdk1.8之后采用尾插法)的方式插入新的散列表
  3. 由于hash表长度发生变化,需要进行hash运算,重新进行散列,hash运算的公式为:index = HashCode(Key) & (Length - 1)

注意,jdk1.8之前采用的是头插法插入节点。这样在多线程并发的情况下会出现线程安全问题,链表形成环状,产生死锁。jdk1.8之后,hashmap采用了尾插法,解决了多线程并发的情况下产生死循环的问题。

2.hashTable的低效率

HashTable的操作几乎和HashMap一致,主要的区别在于HashTable为了实现多线程安全,在几乎所有的方法上都加上了synchronized锁,而加锁的结果就是HashTable操作的效率十分低下,几乎已经被淘汰

3.支持并发操作的CurrentHashMap

hashmap在单线程时可用,但在多线程时可能会出现线程安全问题,HashTable虽然是线程安全的,但效率低下。那在多线程时就需要一个线程安全的Map,CurrentHashMap再合适不过了。currentHashMap在jdk1.8 和jdk1.7的实现有些差别

在Jdk1.8之前,采用数组+Segment+分段锁的方式实现

61685282088_.pic.jpg

Segment是一种可重入锁,在ConcurrentHashMap里扮演重要角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里面包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里面包含一个HashEntry数组,每个HashEntry是一个链表结构。每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

在Jdk1.8后,currenthashmap发生了以下变化:

  1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。

  2. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承ReentrantLock。 Jdk1.8之后则采用CAS+Synchronized保证线程安全。

  3. 锁的粒度:原来是对需要进行数据操作的Segment加锁。现调整为对每个HashEntry数组元素加锁,大大提高了并发性能。

  4. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。

  5. 查询时间复杂度:从原来的遍历链表(n),变成遍历红黑树(logN)。