浅谈ConcurrentHashMap(基于JDK1.7)
- ConcurrentHashMap简介
ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现
ConcurrentHashMap在并发编程的情况下使用率非常高
相较于HashTable每次添加的重量级锁synchronized,锁的粒度更细
1.7的ConcurrentHashMap的value是不支持为null的,这与添加是对值进行非空校验有关
- ConcurrentHashMap的数据结构
整体结构就是通过一个个的分段锁来进行并发时的数据隔离
同时Segment锁继承自ReentrantLock,同样继承了他可重入的特性,默认的concurrentlevel为16,代表最大支持十六个线程并发执行
- 首先介绍Segment和HashEntry
//对于没一个segment数组来说,里面都维护了一个HashEntry数组
//在并发的情况下,操作不同的segment是不需要考虑竞争锁的问题的
final Segment<K,V>[] segments;
//每个segment数组维护了这么一个HashEntry数组
transient volatile HashEntry<K,V>[] table;
//hashentry的属性
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
//其他省略
}
//segment的构造方法
public Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
//他来了他来了!ConcurrentHashMap的构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
//这里可以这样理解,就是segment的最大取值
//我们传入的再大,最高也就是最高限值取值,也就是65535
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
//2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
//这里就是对其次幂的一种初始化操作
int sshift = 0;
//ssize 为segments数组长度,根据concurrentLevel计算得出
//这里同样可以认为是对数组长度的初始化
int ssize = 1;
//这里就是循环对来对次幂进行递增,并且对数组长度进行2次幂的计算
//最终得到的,数组长度一定是一个2的次幂数值
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//segmentShift和segmentMask这两个变量在
//定位segment时会用到,后面会详细讲
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 计算cap的大小,即Segment中HashEntry的数组长度,
// cap也一定为2的n次方
//这里c代表着每个segment应该放置的HashEntry数组长度
int c = initialCapacity / ssize;
//到这一步可能大家会有一个误解,刚才是整除了,为什么有乘回来判断大小呢
//这里是因为可能有余数,并没有加上,但又不能不要,所以就累加了回来
if (c * ssize < initialCapacity)
++c;
//这里cap代表着segment存放数组的最小取值
int cap = MIN_SEGMENT_TABLE_CAPACITY;
//判断只要他的数值小于我们之前计算的存放数量,就乘2,最后得到的一定是一个2的次幂的数
while (cap < c)
cap <<= 1;
// 创建segments数组并初始化第一个Segment,
// 其余的Segment延迟初始化
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);
this.segments = ss;
}
put方法
public V put(K key, V value) {
Segment<K,V> s;
//这里就是对value的校验,这里表明了value的数值不能为null
if (value == null)
throw new NullPointerException();
//hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
int hash = hash(key);
//返回的hash值无符号右移segmentShift位与段掩码进行位运算,
// 定位segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)
// nonvolatile; recheck
UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
// in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
PS:此处是借鉴别人的,没想出来该怎么描述
put方法具体实现
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),其实也就是起到了一个预热的作用,以求获取锁之后能够快速的定位到位置,若找不到,则创建HashEntry。
// tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
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,可以看到,
//这个hash值在定位Segment时和在Segment
//t中定位HashEntry都会用到,
//只不过定位Segment时只用到高几位。
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;
//若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
//如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,
//这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,
//这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//获取锁失败,初始时retries=-1必然开始先进入第一个if
if (retries < 0) {//<1>
if (e == null) { //<1.1>
//e=null代表两种意思,第一种就是遍历链表到了最后,仍然没有发现指定key的entry;
//第二种情况是刚开始时确实太过entryForHash找到的HashEntry就是空的,即通过hash找到的table中对应位置链表为空
//当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,
//然后进行循环尝试获取锁,在循环次数还未达到<2>以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,
//那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,
//所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))//<1.2> 遍历过程发现链表中找到了我们需要的key的坑位
retries = 0;
else//<1.3> 当前位置对应的key不是我们需要的,遍历下一个
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {//<2>
// 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁,
//之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对系统性能有消耗的,
//这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {//<3>
// 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
rehash
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)
newTable[idx] = e;
else {
//下面还存在链表的时候
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//会for循环这个链表
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}//这个for循环就是找到第一个后续节点新的index不变的节点。
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//第一个后续节点新index不变节点前所有节点均需要重新创建分配。
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
在我们进行各种操作的同时,经常用到了Unsafe类本地方法
关于ConcurrentHashMap的实现,不论是在jdk1.7还是jdk1.8版本中ConcurrentHashMap中使用的最为核心也是最为频繁的就是Unsafe类中的各种native本地方法。主要的几个方法是Unsafe.putObjectVolatile(obj,long,obj2)、 Unsafe.getObjectVolatile、 Unsafe.putOrderedObject等
其实可以知道的就是对这些方法添加了读写屏障,保证了每次读到的一定是内存数据而不是缓冲保存的数据,同时也保证了指令不会被重排序