1.1、hashMap类介绍
Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。HashMap 是Map的一种实现,其底层是基于 数组 + 链表 组成的,在数据没有发生冲突之前放在一个数组中,如果某一个数据在hash后发生冲突,则将数据数组发生冲突的地方建立一个链表,将发生冲突的数据依次放入这个链表中。
1.2、hashMap源码分析
我们在初始化一个hashMap的变量时只做了一件事,就是初始化了这个hashMap变量的负载因子为0.75,取这个值原因有
- 我们的hashmap的容量为2的倍数,一般情况下是可以被4整除,这样计算得到的threshold一般情况下也为一个整数。
- 这个是结合hashmap中发生冲突的概率和hashmap扩容造成性能消耗而得出较为合适的数值,如果负载因子太大,趋近于1,则会造成hashmap中已经冲突非常严重了,缺还是没有进行扩容。如果负载因子太小,则经常冲突还不严重就频繁进行扩容造成系统性能和内存的无端消耗
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
上面putval函数是默认在hashMap中添加数据的方法,这边对各个参数说明一下
- key的hash值,具体计算方法这边不介绍了,这个hash值的特点时候同一个key对应的hash值必定一致,不同的key对应的hash值也有可能会相等
- key值
- value值
- onlyIfAbsent,如果为true,则如果添加的数据
- evit,如果为false,则该表处于创建模式
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //1
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //2
tab[i] = newNode(hash, key, value, null);
else { //3
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //4
resize();
afterNodeInsertion(evict);
return null;
}
- 代码注释1处:如果hashMap类中的变量table还是null或者长度为0,则调用resize函数来创建一个table,可以看到,hashmap的表示在put时才进行创建的,不是在初始化里创建的。具体源码在下面
- 代码注释2处:然后利用(n - 1) & hash来进行求余操作,对求得的值判断当前的table上有没有值,如果没有值,则新建一个Node放入到table中
- 代码注释3处:如果在table中已经存在了一个值,则表示发生了冲突(注意发生冲突的条件是key的hash值对table的容量求余后的值一致)。如果发生冲突的节点数据和原来的节点的hash值,key值地址相同或者数值相同的话,根据onlyIfAbsent这个值,如果是false则将新的值替换掉之前的值,如果为true则不进行替换。如果key的地址和数值都不相同,则需要建立一个链表,将新的节点数据放入到链表中。这个插入链表的方式是尾插法(jdk版本1,8),如果这个链表的长度大于等于8个,则会将这个链表变成红黑树结构,加快查询的速度。
- 代码注释4处: 如果当前hashmap中的节点数量大于等于threshold,则需要进行扩容,一开始的hashmap的Node数组大小为16,threshold为12,则添加到第13个节点时会发生扩容。也是调用resize函数来进行扩容。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { //1
if (oldCap >= MAXIMUM_CAPACITY) { //2
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //3
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 4
newCap = oldThr;
else { // 5
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 6
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) { // 7
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
/*扩容前的数组中的每一条链表因为hashMap表扩容了,需要重新计算放置的位置,但是放置的地方只有两个地方
例如本来我的hashMap的长度为4,则如果在0这个位置下节点的key的hash值有0,4,8,12,则在扩容后的
长度变成了8,则原本0位置下的链表上的节点只有可能在两处地方,就是0这个位置和4这个位置
这边loHead和loTail用来表示原来部分的情况,我们将扩容后的hashMap逻辑上分成两个部分,
0-3这边用loHead和loTail来处理,4-7这部分用hiHead和hiTail来处理*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { //8
if (loTail == null)
/*这边的e就是在hashmap上的数组部分的节点,还没有到链表部分*/
loHead = e;
else
/*更新next,用来连接*/
loTail.next = e;
/*这一处的赋值一定要好好理解,这边第一次赋值时的作用是将这个loTail和loHead指向同一个地址
之后的作用是不断更新尾节点,就是这样将从头结点依次将适合的节点用next连接下去*/
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
/*经过上诉处理后这个loHead利用next组成了一条新的合适的链表*/
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这个函数里有许多if-else结构,其调用过程中有多中情况。
- 初次调用:初次调用时为初始化创建一个table,直接将进入到代码注释中的5,初始化了newCap为16,则相应的newThr为12,然后创建了一个Node数组,大小为16。
- 进行扩容时调用,如果扩容后数组大小小于1 << 30,进入到代码注释3处,将threshold和newCap扩大为原来的两倍,然后进入到代码注释7,这边主要讲将扩容前的数据节点正确放到扩容的hashmap表中,这边看下代码注释处8,这边e.hash & oldCap和上面的(n - 1) & hash来进行求余操作可是不太一样了哦,这边e.hash & oldCap如果为0,则表示最高位为0,则添加的位置还是为扩容前的位置。如果e.hash & oldCap为1,表示最高位为1,则这个节点放入的位置为扩容后新增加的空间的位置。并且利用这种方式已经解决了jdk1.7中的多线程使用hashmap会造成死锁的可能。jdk1.7中的头插法,jdk1.8中使用的是尾插法,就是新的节点插入到链表的末尾。
2.1、concurrentHashMap介绍
concurrentHashMap是线程安全的容器,因为在hashMap中如果多线程操作可能会发生不确定现象,在hashTable中确实也是线程安全的,但是在hashTable中是在每一个函数前加上了synchronized关键字,这是一把悲观锁,对系统性能的影响较大因此hashTble的性能是不高的。那有没有一种容器是线程安全的并且不使用悲观锁,concurrentHashMap就是这样的容器,concurrentHashMap中实现线程安全使用了乐观锁,乐观锁不同于悲观锁,悲观锁是在不确定一个数据是否可能会改变的情况下都默认这个数据会进行改变,在我们操作的时候一直锁住数据。但是乐观锁是在不确定一个数据是否可能会发生改变的情况下默认其是不会改变的,只是在我们需要获取或者设置的时候去判断一下这个数据是否发生改变,如果没有发生改变时,进行设置,否则就是这个数据已经发生改变,不进行设置。
2.2、concurrentHashMap源码分析
下面我们根据putVal函数来看下concurrentHashMap中是如何实现线程安全的
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
/*获取hash值*/
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
/*这种情况下就是还没有创建table,需要先初始化这个table,initTable函数在下面会详细分析*/
if (tab == null || (n = tab.length) == 0) //1
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //2
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) //3
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { //4
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //5
return null;
}
主要过程是(下面的序号对应于代码中的注释序号处)
- 1、根据key获取hash值,然后判断concurrentHashMap中的table是否已经创建,如果没有创建需要初始化一个table,可以看出在concurrentHashMap中初始化操作也是在putval中进行的,在第一次添加的时候如果发现table还是未初始化状态时会进行初始化。
- 2、然后根据hash值对容量n求余,判断对应的数组中是否已经有节点了,如果还没有节点,则调用casTabAt函数来进行添加,这个函数内部也是使用了乐观锁,是先去判断数组的对应位置是否还为null,如果还为null,就新增,如果不为null,说明已经有其他线程进行添加了,直接返回即可。
- 3、如果当前hash值对容量求余后发生数组中对应的位置已经有节点了,并且这个节点正在扩容移动状态,则这个线程就协助扩容操作。这个协助扩容的过程下面会进行详细分析
- 4、如果该数组中没有相对应的节点并且节点也没有进行扩容操作,则进行添加节点操作,这个插入操作和hashmap几乎一致,只是在一开始多了一个synchronized(f)来锁住这个节点,不能再添加的时候其他线程再对这个节点进行操作。其中也会有是否是红黑树结构和是否需要升级到红黑树结构的判断。
- 5、会更新当前concurrentHashMap中节点的个数,并且判断是否是要扩容的处理。详细下面会分析
如何初始化concurrentHashMap的table
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 首先判断table是否已经进行初始,这边用了while,这个不同于if,好处在于如果第一次我因为table为初始化进去了,但是因为其他线程已经在初始化而没有进行初始化操作,但是其他线程在未进行完初始化cpu时间用完了,也没有进行初始化,这里用了while可以继续进行初始化,否则如果用if,则如果一次失败则没有了重试的机制。
- 然后如果sizeCtl<0说明已经有线程在进行初始化了,这个线程就不需要在去进行初始化,就直接调用Thread.yield()来让出本次cpu的占用,但是注意这只是让出本次而已,并不代表退出本次线程,在下一个cpu的竞争占用时这个线程仍然回去竞争。如果在下一次cpu竞争时这个线程竞争到了,会接着运行,在上次运行到Thread.yield()继续接下去运行,可以看到会回到while循环,如果这时候发现初始化结束了,那么最终会调用到break来结束循环,从而结束初始化。
- 如果没有进行初始化,那么就调用compareAndSwapInt来修改sc,如果sc的值和期望值相同,并且修改成功compareAndSwapInt会返回true,这边U.compareAndSwapInt(this, SIZECTL, sc, -1)中sc这个参数就是我们的期望值,-1表示我们希望替换的值,所以compareAndSwapInt会先去对比一下偏移量为SIZECTL的内存上的值是否和sc值相同,如果相同再将这个值替换为-1,如果都能成功则返回true。
- 在compareAndSwapInt返回true在多线程情况下可不一定是线程安全的,因为可能同时又多个线程获取到偏移量为SIZECTL的内存上的值,然后去和sc判断,都发现和sc这个参数的值相同,判断sc没有进行修改,同时将sc这个值设置为-1,这些几乎同步的线程都进行table的创建,其实这个并没有太大的影响,只是多创建了几个Nod数组,之后会被垃圾回收器回收。但有一种情况下可能会造成问题,就是一个线程刚进入U.compareAndSwapInt(this, SIZECTL, sc, -1)的判断语句后,其cpu时间用完了,被挂了起来,这时候其他线程继续进行初始化,并且新加入了一些节点进去,但之后这个被挂起来的线程有重新占取了cpu,这个时候如果没有(tab = table) == null || tab.length == 0这个判断的话,这个重新起来的线程就会重新创建一个新的table,那么之前添加的节点就会被删除掉,所以这边一定要二次判断(tab = table) == null || tab.length == 0。
- 创建table的过程很简单,其中sc = n - (n >>> 2)这个sc就是n的阈值,n>>>2就是n/4,得到的sc就是3/4*n
如何更新concurrentHashMap节点个数
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/*counterCells存储的都是value为1的CounterCell对象,而这些对象是因为在CAS更新baseCounter值时,由于高并发而导致失败
执行fullAddCount(x, uncontended)方法,这个方法其实就是初始化counterCells,并将x的值插入到counterCell类中,而x值一般也就是1
最终将值保存到CounterCell中,放到counterCells里。*/
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
/*遍历counterCells数组,sum累加CounterCell.value值来获取最后的sum,其实这边的value一般情况下就是为1
遍历就可以获取到因为CAS而add失败的次数,因为只有在第一次能够设置成功,其余若要添加因为cas的存在都会失败
所以这里可以统计concurrentHashMap中的节点的个数-1*/
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
/*要注意sizeCtl表示的concurrentHashMap表的阈值,而s表示concurrentHashMap的个数-1,如果s >= (long)(sc = sizeCtl)
表示concurrentHashMap表中的节点个数已经超过了阈值,需要进行扩容*/
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
/*正在初始化*/
if (sc < 0) {
/*表示扩容已经结束*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
/*协助扩容,sc表示进行扩容的线程个数,每多一个线程协助,则sc+1*/
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
/*第一个开启扩容的线程,初始化sc为(rs << RESIZE_STAMP_SHIFT) + 2*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
/*实时更新节点的个数*/
s = sumCount();
}
}
}
因为多线程的特殊性,所以统计concurrentHashMap中节点的个数也不能按照hashmap那样构建一个计数器,因为多线程情况很容易造成这个计数器的值是偏小的。concurrentHashMap构建了一个技术类CounterCell,利用CAS对同一个数进行swap只有一次可以成功的特性,之后的不同线程每需要添加节点后需要更新节点的个数就会去调用U.compareAndSwapLong,失败了会在CounterCell创建一个新的节点,之后只需要遍历CounterCell的节点个数就可以知道concurrentHashMap中的节点个数。
需要注意在concurrentHashMap中如果已经线程在进行扩容操作,其他线程如果需要插入节点,会先去协助扩容操作,只有在扩容操作结束后才会添加节点。
concurrentHashMap如何进行扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
/*多线程扩容,每核处理的量小于16,则强制赋值16,这边也可以认为就是数组中的16个桶*/
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
/*这边nextTab就是扩容后的table,如果还没有生成,则需要生成一个*/
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; //将新生成的table赋值给concurrentHashMap的nextTable变量,这样其他线程也可以直接得到这个table
transferIndex = n; //n的值为扩容前的table的容量
}
int nextn = nextTab.length; //扩容后的table的容量
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //是否进入到下一个桶的标志
boolean finishing = false; // 扩容是否结束的标志
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
/*这个while循环是管理当前线程读取相对应的桶的区间*/
while (advance) {
int nextIndex, nextBound;
/*这个是判断当前的线程读取的桶的下标是否还在规定的区间*/
if (--i >= bound || finishing)
advance = false;
/*transferIndex<=0表示已经所有的桶都已经移到争取的位置了,扩容结束*/
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
/*这个表示当前读取的桶的下标已经超过了规定的区间,这个线程可以去读取下一个区间的桶了,在一开始刚读取时也会进入
*/
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
/*i < 0表示所有的桶都已经移动到合适的地方,可以结束扩容了,但是真正结束还需要验证finishing字段*/
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
/*结束扩容*/
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //更新阈值
return;
}
/*将表示进行扩容操作的线程数量减一*/
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
/*如果当前的节点在table中不存在,直接插入*/
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
/*如果当前节点正处在扩容状态,则继续扩容*/
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
/*转移一个桶的节点,包括这个桶下面的所有链表上的节点*/
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
具体转移的过程其实和hashmap的转移方式一致,都是通过构建高位和低位的两条链表,遍历一个桶下链表的所有节点,将节点在这两条链表选择合适的链表进行连接。最后将这两条链表挂在新table合适的桶上。对应上面的就是synchronized关键字包裹的代码。
其实这边我主要讨论一个问题,就是concurrentHashMap中如何进行多线程协助进行转移操作,如何才能使多线程之间同时转移节点不会互相影响?在上面代码中可以看到concurrentHashMap在transfer不是一次性地让一个线程进行转移所有桶上的节点,而是进行了分段
U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0)
对应于代码里的这个函数,默认的stride为16,也就是程序默认情况下会为每一个线程分配16个桶,也就是说,如果小于等于16个桶,那么也不可能会有两个线程进行扩容转移节点。这边图中我简化了一下,假设一次分配为4个桶的数量,扩容前一共为16个桶。在上面nextIndex = transferIndex中获取到了nextIndex为16, 那么nextBound在第一次时获取的值为12,stride为4。同时更新transferIndex为12。如果有另一个线程进来协助,那么可以知道新获取的nextIndex为12,那么nextBound的值为8,也就是说明了下一个线程的操作区间为8-12。那么还有一种情况就是一个线程转移较快,那么这个线程会继续去申请下一段区间,在 if (–i >= bound || finishing)可以判断当前的桶的下标i是否已经小于最小桶的下标,如果小于了,则继续申请。
3、总结
concurrentHashMap所实现的功能实际上和hashmap是一致的,但是为了适应多线程的安全性,concurrentHashMap做了许多处理来保证其安全性,用的最多的当然是乐观锁CAS的使用,同时许多地方也多了一次看似重复的判断,但是如果没有这次看似重复的判断,在多线程环境下很容易造成数据的丢失。
concurrentHashMap在添加节点时,可能表正处在扩容转移节点状态。这个多个线程会协助先处理表的扩容操作,直到扩容结束后才会进行添加节点。而hashmap因为只适用于单线程,添加节点和扩容转移节点同一时间只会发生一个,所以没有这方面的处理。
concurrentHashMap在1.7版本是使用了分段锁,这个在1.8已经不再使用了,转而直接使用synchronized (f),其中f一定是链表的头结点,即该元素在Node数组中。所以这里锁住的是hash冲突那条链表。这随着Node数组越来越长,concurrentHashMap的并发度也越来越高。这比之前的分段锁,并发度为固定的要好。同时不使用ReentrantLock是因为在线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效。具体可以用查看这篇文档ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock
最后在引用一篇文章,这篇对concurrentHashMap的总结也非常不错jdk1.8的HashMap和ConcurrentHashMap