为什么要使用ConcurrentHashMap
HashMap是线程不安全的,因为在put操作时可能会出现数据被覆盖的情况(JDK8),在JDK7中还存在扩容时产生死循环的问题。
。而使用线程安全的HashTable效率又非常低下,因此可以使用ConcurrentHashMap。
HashTable效率低下是因为使用synchronized来保证线程安全。当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
JDK1.7中的ConcurrentHashMap
数据结构
ConcurrentHashMap是Segment数组 + HashEntry数组结构,HashEntry存储键值对。
一个ConcurrentHashMap包含一个Segment,Segment里包含一个HashEntry数组,每个HashEntry数组中是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。每个Segment元素相当于一个小的HashMap。
ConcurrentHashMap类中包含一个Segment<K,V>类型的数组,名为segments。Segment类中包含一个HashEntry<K,V>类型的数组,名为table。HashEntry 类中包含键值对的内容以及下一个HashEntry的地址,HashEntry 就相当于链表的节点,节点中存储的内容是键值对。
一个ConcurrentHashMap中只有一个Segment<K,V>类型的segments数组,每个segment中只有一个HashEntry<K,V>类型的table数组,table数组中存放一个HashEntry节点。
对源码的分析先从小到大:HashEntry–> Segment–> ConcurrentHashMap。
HashEntry 类:
HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//只列出主要部分
HashEntry就是ConcurrentHashMap数据结构中最小的存储单元,它就是对应一个个的<k,v>节点,它的内部相对简单。
Segment 类:
关于Segment内部的实现相对HashEntry肯定是要复杂一点的,这里分两部分介绍,首先介绍它内部的成员变量。
成员变量
//scanAndLockForPut中自旋循环获取锁的最大自旋次数
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//存储着一个HashEntry类型的数组
transient volatile HashEntry<K,V>[] table;
/存放元素的个数,这里没有加volatile修饰,所以只能在加锁或者确保可见性(如Unsafe.getObjectVolatile)的情况下进行访问,不然无法保证数据的正确性
transient int count;
/存放segment元素修改次数记录,由于未进行volatile修饰,所以访问规则和count类似
transient int modCount;
/当table大小超过阈值时,对table进行扩容,值为(int)(capacity *loadFactor)
transient int threshold;
/负载因子
final float loadFactor;
Segment的rehash扩容分析
/**
* Doubles size of table and repacks entries, also adding the
* given node to new table
* 对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,
* 对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点fv,
* 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=fv,即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中
* 这样最好的情况是所有oldTable中对应头结点后跟随的节点在newTable中的新的index均和头结点一致,那么就不需要创建新节点,直接复用即可。
* 最坏情况当然就是所有节点的新的index全部发生了变化,那么就全部需要重新依据k,v创建新对象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list 只有单个节点
newTable[idx] = e;
else {
// Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k