一、ConcurrentHashMap构造方法
ConcurrentHashMap的结构和HashMap有些许的不同。他用一个Segment数组来代替数组,具体用HashEntry来存储数据。
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
可以看到,这里调用无参的构造方法的话,他会去调用具有三个参数的构造方法。
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
int sshift = 0;
int ssize = 1;//ssize是Segment数组的长度
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}//ssize是一个大于等于concurrencyLevel的2的幂次方数
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;//MIN_SEGMENT_TABLE_CAPACITY定义的初值是2
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);//生成一个s0的对象放在Segment[]的第一个位置作为以后存数据的参数
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
initialCapacity:代表的是HashEntry[]数组的大小。初始化默认为16.
loadFactor:加载因子,在判断扩容的时候用到,默认是0.75
concurrencyLevel:并发级别,代表Segment[]数组的大小,默认是16
这三个参数的构造方法内部呢,首先会进行一些健壮性的判断,判断参数的合法性。再去初始化一个sshift和ssize,ssize是在new一个Segment数组的时候作为他的大小,按照给定的并发级别的大小去改变ssize的大小的时候,sshift会记录下ssize左移的次数,并且ssize是一个2的幂次方的数值。而segmentShift和segmentMask这两个变量在定位segment时会用到,我们后面分析。至于下面定义的c,是用来计算cap的大小,而cap是通过位移操作来计算segment下HashEntry数组的大小,cap也一定是一个2的幂次方数。最后先new出一个Segment对象s0,作为以后初始化segment时候数据的参考。不需要再去计算。最后new出Segment数组,方法结束。
二、put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;//j是key所对应的Segment下标的位置
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) //取到segments第j个位置的元素 ,判断是否为空
s = ensureSegment(j);//如果为空,ensureSegment(j)
return s.put(key, hash, value, false);
}
put方法,首先去计判断value是否为null,是就抛出异常,所以ConcurrentHashMap是不允许value值为null的。然后计算key的hash值,保证一下散列性。
int j = (hash >>> segmentShift) & segmentMask;这句代码,hash值无符号右移segmentShift位并与segmentMask进行与操作,最终得到的就是Segment位置。其中,SegmentMask(段掩码)是为了确保散列的均匀性(计算方式为segments数组长度-1,构造函数中有体现);segmentShift计算方式为segmentShift=32-sshift,其中2的sshift次方等于ssize。比如segments数组长度16,2的n次方=16,segmentShift=32-n。UNSAFE.getObject (segments, (j << SSHIFT) + SBASE))这句代码就是去得到segments[ j ]的segment,然后看他是否为null,是就调用ensureSegment(j)。我们再来看看这个方法。
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; //获取k所在的segment在内存中的偏移量也就是Segments[]第k个位置
Segment<K,V> seg;
//(Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))判断Segment[]的第u个位置是否为空,防止并发问题
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {//
Segment<K,V> proto = ss[0]; // use segment 0 as prototype,使用segment[0]作为原型
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//再判断Segment[]的第u个位置是否为空,防止并发问题
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);//创建一个segment对象
//(Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)拿到内存中的值
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//自旋,防止seg为空
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//CAS操作,解决并发
break;
}
}
}
return seg;//返回生成的segment对象
}
首先利用UNSAFE拿到segments[ k ]的segment,看他是否为null,防止出现并发问题。然后拿到Segments[ 0 ]的数据,作为原型。再去new对应的HashEntry数组,然后再判断segments上u的位置是否为空,这里为何要在判断一次呢,因为在执行上方赋值操作和new操作的时候已经浪费了一部风时间,所以要二次检查是否有其它线程创建了这个Segment,如果没有就创建一个Segment,然后再用一个自旋的CAS原子操作方式对segments数组中偏移量为u位置设置值为s,这是一种不加锁的方式,万一有多个线程同时执行这一步,那么只会有一个成功,而其它线程在看到第一个执行成功的线程结果后会获取到最新的数据从而发现需要更新的坑位已经不为空了,那么就跳出while循环并返回最新的seg。
总体来说,这个方法核心思想就是利用自旋CAS来创建对应Segment,这种思想是之后不加锁保证线程安全的一个十分典型的实现方式。除了这个方法关于segments数组还有一些其它实现较为简单的例如:segmentAt、segmentForHash等方法,这些方法就是利用Unsafe中的方法去实现从主存中获取最新数据或是直接往主存中写入最新数据,实现代码逻辑十分简单,对于这些方法不再赘述。
返回到put方法之后,最后执行return s.put(key, hash, value, false)操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//tryLock()方法,如果能获取到锁,直接返回true,否则返回false,不回阻塞,而lock()方法如果获取不到锁,就会阻塞,直到获取到锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//tryLock尝试去获取锁,获取不到调用scanAndLockForPut
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;//算出一个下标值
HashEntry<K,V> first = entryAt(tab, index);//取到tab表中index位置的hashentry
for (HashEntry<K,V> e = first;;) {//遍历当前HashEntry的链表
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {//如果链表上存在和当前插入key一样的key
oldValue = e.value;//保存当前value
if (!onlyIfAbsent) {//onlyIfAbsent是false
e.value = value;//替换value
++modCount;
}
break;
}
e = e.next;
}
else {//如果为空
if (node != null)//如果node已经生成好了,就只需要改一下next指向,因为new的时候next指向null
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);//头插法,生成一个node对象
int c = count + 1;//count是当前segment下面hashentry[]所存元素的个数
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);//扩容
else
setEntryAt(tab, index, node);//利用UNSAFE操作改变内存的值
++modCount;
count = c;//得到当前segment下面hashentry[]所存元素的个数
oldValue = null;
break;
}
}
} finally {
unlock();//解锁
}
return oldValue;//返回oldvalue
}
这个方法首先尝试去获取锁,获取不到调用scanAndLockForPut(key, hash, value)
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);//this表示当前segment对象,此方法是取hash值对应下标的元素
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node,
while (!tryLock()) {//尝试获取锁的过程中做一些其他事情
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {//表示要么first为null,要么遍历到尾节点
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);//new一个hashentry
retries = 0;//为了只new一个对象
}
else if (key.equals(e.key))
retries = 0;//有可能不用new,走下一个逻辑
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {//retries重试次数,大于一定值,不让while太多次
lock();//获取锁
break;//退出
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {//偶数次数的时候,去判断当前链表是否发生了变化,如果发生了变化,重新遍历这个链表
e = first = f; // re-traverse if entry changed
retries = -1;//把值改为-1 重新遍历
}
}
return node;
}
我们首先看这个循环,就是当获取不到锁的时候才会执行循环体里面的内容,这样可以再里面执行一些其他内容而不是去等待。然后用retries 来控制循环的次数使它不会循环太多次,不然会大量占用CPU的资源。当他大于MAX_SCAN_RETRIES值是就获取锁然后退出。当retries小于0的时候,会去遍历链表,遍历到最后一位的时候,new出node然后返回。大体意思就是如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
返回之后,会继续执行put中的逻辑,这里面的逻辑就比较简单了,主要是去遍历链表,找到相同的key就会去执行覆盖value的操作,如果没有找到,就去new一个HashEntry,然后去改变一些属性,我们主要去看一下里面的扩容方法。
private void rehash(HashEntry<K,V> node) {//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 { // 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;
lastRun = last;//lastRun记录最后放到一条lian'biao'shang
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
我们可以看到,新数组的大小是旧数组大小左移一位得到的值,也就是扩容为原来的两倍。然后重新计算一下阈值,再去new一个新的HashEntry数组。大小为oldCapacity << 1。然后就是遍历老数组然后转移了。如果当前的HashEntry只有一个节点,那就直接放到新的HashEntry数组上,如果不是,那就遍历当前链表,计算一下哪几个节点是放到新数组的一条链表上,然后放到新数组上,然后把剩下的节点再放到新数组上,最后把当前传入的头节点放到新数组对应链表位置的第一个节点上,然后把新数组赋值给table,扩容结束。
二、get方法
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get方法比较简单,首先获取key的hash值,然后获取到对应hash值存储所在segments数组中内存偏移量,拿到这个segments数组的第u个位置的值,如果不为空,循环当前Segment下面的数组,如果得到相等的key,返回对应的value,否则返回null
二、remove方法
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);//拿到链表头节点
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
对于remove方法,是直接调用Segment中的方法,逻辑已经介绍过,这里针对这些方法不再进行介绍。