------transfer

扩容是按stride为跨度进行,transferIndex属性是一个互斥量。一个线程进入transfer方法,先保证初始化,进入for循环,获取transferIndex,cas方法向下获取stride个位置的跨度,直到跨度到尽头,说明所有的跨度,要么已经扩容结束,要么正在被扩容,本线程退出。

------addCount方法分析:

2258行的if成立意味着counterCells没有初始化或者baseCount并发过高

2262行的if成立意味着counterCells没有被初始化,或者当前线程对应的CounterCell没有初始化,或者当前线程对应的CELLVALUE并发过高,

反正不管怎样,只要两个都成立,就说明这个数量是没加上去,进入fullAddCount

fullAddCount中,判断初始化counterCells、CounterCell,反正这个数量一定是要加上去。

出来之后,看看check,在putVal中这个check是binCount,也就是节点最后添加的位置,反正是大于0,是肯定要往下走的(但是remove、replace、clear三个方法传的是-1,这也很容易理解)

往下走:在一个无限循环中,2278行的判断是看是不是还有跨度等待扩容。如果有,参与扩容,如果没有,退出。

------考虑一种极端情况,会不会发生连环扩容?

因为当前线程从transfer退出之后,有可能还有其他线程在扩容最后的几个stride,此时的putVal方法会直接把新节点添加到newTable中,那么会不会此时newTable的元素也满了,从newTable transfer到newnewTable?

A:不会,虽然是往newTable添加元素,但是元素总数还是存到sumCount以及countCell中,这些是和map实例相关,和newTable无关的。最关键的是,就算是调用了transfer,方法第一个参数永远是table,也就是old Table,那么进入transfer之后会直接发现stride已经分配到最后一个而返回。只有当所有stride都扩容完毕了,map的table属性才会指向newTable。

------put,remove和transfer的并发冲突

1、如果f=tabAt(tab, i) 为null,两个线程都用cas方法,put方法放入新node,transfer方法放入fwd、remove直接return。

2、如果f为MOVED,put和remove方法会进入transfer,返回值是新tab,在新tab里进行操作。注意如果f是moved,看transfer的代码的话,原table中的f已经在newTable中被扩容(一变二)完成了,所以在新tab中的put和remove操作是安全的。

3、如果为其他情况,也就是f为真实节点,解决办法是加锁,put和remove的加锁之后,还要再判断一次if (tabAt(tab, i) == f),这是因为transfer的加锁之后,一变二,再把f处变为fwd。如果transfer加锁之后,再释放,等到put拿到锁,f还是不是tabAt(tab, i)已经不一定了。当然put和remove也会有这样的冲突。

------读有没有加锁?

经常会有公众号文章说读因为有volatile,就没有加锁,这样说是不严谨的,concurrentHashMap一共有三种主要的数据结构:哈希表、链表、红黑树,前两者的put和remove操作对读写并发来说是安全的,但是红黑树的旋转操作会使得并发出现问题,所以点开TreeBin的find方法,会发现:如果写锁一直没有释放,考虑到在链表转换成红黑树的时候,之前链表的前后关系还是保存的,那么此时可以用链表的遍历,这样虽然效率有点低,但是总比等锁要高,但是如果获取到了读锁,那么就用红黑树的二分法从root查找,此时写操作是不能做的。