ConcurrentHashMap的实现原理
一、为什么要使用ConcurrentHashMap
- 实际工作中hash表是用使用很频繁的一种存储技术,常使用的是HashMap 和HashTable,但是在多线程环境下,使用HashMap会导致死循环,导致cpu接近100%,死循环的原因是多线程会导致HashMap的Entry链表形成环状数据结构,(一般是扩容时resize操作导致)一旦形成环状数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
- HashTable的低效率性,HashTable的实现原理跟HashMap类似,HashTable的key和value不允许为null,且所有get和put操作,都加上了Synchronized锁,如果执行put操作,会锁住整个表,在多线程环境下,频繁的切换锁,串行化执行,效率较很差。
- ConcurrentHashMap的出现就是把HashTable的整个锁,给分成一个一个的段Segment,锁与锁之间是独立的,每个锁负责维护一部分HashEntry,当一个线程访问其中的一个段数据时,其他线程也可以访问其他段的数据。
二、ConcurrentHashMap 源码解析
ConcurrentHashMap的主干结构就是Segment,以前是Segment的主要字段定义
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;// 链表数组,
transient int count; // Segment中元素的个数
transient int modCount; // 操作table大小的次数
transient int threshold; // 用于扩容的阀值
final float loadFactor; // 负载因子
}
HashEntry数组为储存元素的最小单元,一个Segmeng维护一个HashEntry数组。
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况。
ConcurrentHashMap初始化
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
*// **2的sshift 等于 ssize,***,代表ssize左移的次数
int sshift = 0;
// ssize为segment的长度,根据concurrencyLevel 计算得出
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//segmentShift和segmentMask这两个变量在定位segment时会用到
//segmentShift 用来定位参与散列运算的位数
// segmentMask 是散列运算的掩码
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
//计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建segment 数组,并初始化一个s0.
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
initialCapacity 初始化容量,
loadFactor 负载因子,
concurrencyLevel 代表ConcurrentHashMap内部Segment的数量,默认为16,concurrencyLevel 一经指定,是不可变的,也就说初始化Segment的数量就是一定的,如果ConcurrentHashMap需要扩容,只需要对Segment中的HashEntry数组进行扩容就行,这样做的好处是不需要对整个ConcurrentHashMap进行rehash,只需要对Segment中的元素rehash。
concurrencyLevel 的最大值是 1 << 16,最大值是为65535,也就是最大的并发数65535,
Segment数组的大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。
初始化过程总结:主要是通过初始值、负载因子、并发级别,确定Segment的大小。
三、定位Segment
能够快速的定位Segment,有利于快速的存取元素。
由ConcurrentMent初始化的时候,可知我们得到了全局变量 segmentShift :2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
segmentMask :段掩码,,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
由此可知ConcurrentHashMap 在put元素时会经过两次hash,第二次hash有利用减少冲突,使元素能够均匀的分布在Segment上。
四、put方法
put方法分为两步,一是 定位Segment,然后往Segment中put元素。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
Segment的put 也分为两步,一是先判断是否需要对HashEntry进行扩容,二是定位要添加元素的位置,然后将其放在HashEntry里。
1、判断扩容
在插入元素前先判断Segment里HashEntry数组是否超过容量,如果超过阀值,则扩容
2、如何扩容
先把元素的数组扩容为容量的两倍,然后把原数组的元素,通过再散列插入大新数组,只会扩容其中的一个Segment。
五、get方法
get方式源码就不贴了,可自行在jdk1.7环境下查看,get操作不需要加锁,get方法中获取的变量都是用volatile修饰的,volatile可以在多线程环境下保证内存的可见性,所以不会读到过期数据。
六、remove方法
remove方法首先也需要确定元素位置,然后将待删除元素位置前的元素,统一复制一遍,重新一个一个的插入新的链表中,
七、size方法
Segment的全局变量count是一个volatile类型的,虽然在多线程环境下可保证可见性,但是统计每一个Segment的大小,需要想加,加的过程中count 可能发生变化。所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment的大小,如果统计过程中,count发生了变化,则再采用加锁的方式来统计。如何判断容器发生了变化,通过比较modCount 和size的大小。
八、总结
ConcurrentHashMap在多线程环境下,具有很高的并发能力,能够提高生产效率。