HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
因为初始化数组完成后数组元素都还是null值,以后每一次添加一个元素的话,需要封装为entrySet对象,还需要对entrySet数组的大小重新计算,如果把第一次的计算结果全部存储到S0中,那么以后的话只需要直接拿来使用即可,不需要重新计算。虽然Segment对象不同,但是对象中属性内容其实是一样的。
上面的源码分析了 ConcurrentHashMap 在 put 一个数据时的处理流程,下面梳理下具体流程。
上面探究了获取 Segment 段和初始化 Segment 段的操作。最后一行的 Segment 的 put 方法还没有查看,继续分析。
遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。
这里面的第一步中的 scanAndLockForPut 操作这里没有介绍,这个方法做的操作就是不断的自旋tryLock()获取锁。当自旋次数大于指定次数时,使用lock()阻塞获取锁。在自旋时顺表获取下 hash 位置的 HashEntry。
ConcurrentHashMap 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表头插法插入到指定位置。
有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。
算法思路:V是共享变量,我们拿着自己准备的这个E,去跟V去比较,如果E == V ,说明当前没有其它线程在操作,所以,我们把N 这个值 写入对象的 V 变量中。如果 E != V ,说明我们准备的这个E,已经过时了,所以我们要重新准备一个最新的E ,去跟V 比较,比较成功后才能更新 V的值为N。
如果多个线程同时使用CAS操作一个变量的时候,只有一个线程能够修改成功。其余的线程提供的期望值已经与共享变量的值不一样了,所以均会失败。
ABA问题:E和E2对比相同是不能保证百分百保证,其他线程没有在自己线程执行计算的过程里抢锁成功过。有可能其他线程操作后新E值和旧E值一样!
ABA问题解决:在E对象里加个操作次数变量就行,每次判断时对比两个,E和操作次数就OK了,因为ABA问题中就算E相同操作次数也绝不相同
从源码中可以发现 ConcurrentHashMap 的初始化是通过自旋和 CAS操作完成的。里面需要注意的是变量sizeCtl,它的值决定着当前的初始化状态。
get 流程比较简单,直接过一遍源码。2023年2月28日1时26分52秒