一、Hashtable vs ConcurrentHashMap总体比较
1.Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
2.Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
3. 1.8 之前 ConcurrentHashMap 使用了 Segment + 数组 + 链表的结构,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
①演示并发度
②演示 Segment 索引计算
③演示扩容
④演示 Segment[0] 原型
4. 1.8 开始 ConcurrentHashMap 将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
①比较 7 的 ConcurrentHashMap
②理解 capacity 和 factor
③演示并发 put
④演示扩容,说明三个问题 forwardingNode ,扩容时的 get,扩容时的 put
⑤关于超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容
二、Hashtable特点和演示
Hashtable初始容量为11,每次扩容是上一次容量*2+1
放入1,2,3,4,5,6,7,8
在放入9就出现扩容
他的hashCode计算不需要经历二次hash,因为他的容量是质数,有比较好的hash分散性,我们看看源码,hash按位与,为了保证正数取余
三、Java7版本的ConcurrentHashMap特点和演示
1、并发度
蓝色外层的大数组就是Segment数组,Segment数组里面套一个小数组HashEntry,他等价我们普通hashmap的结构,如果出现索引冲突,他会构成一个链表。
将并发度改成8,数组长度随着变化,
那capacity容量是决定小数组的长度,小数组长度计算是容量除以并发度,如果容量没有并发度大,他就会以最小值2创建。
2、Segment索引计算
它是需要二次hash,也就是一些移位,异或的操作,使得hash分布更加均匀,我们已下图为例,二次hash如何变成segment下标。
将二次hash值输入计算器,本次的并发度为16,2的4次方,所以看他的高4位,则为1100,十进制为12
那我们再看看小数组的计算方式
小数组大小为2的1次方,看他的低一位,也就是0
3、演示扩容
外层segment是不会扩容,会变化的是里面的小数组,当元素个数超过3/4会触发扩容,每次翻倍
值得注意的是,他扩容只会扩容当前索引下的小数组,其他的不会扩容
4、演示segment[0]原型
创建时,发现除了0其他索引没有小数组,那0索引的小数组作用是什么,它是为了作为原型,其他小数组创建将以这个为原型
看下图,如果这个时候我们再创建一个新的小数组
他就会以segment[0]为原型
四、Java8版本的ConcurrentHashMap特点和演示
1、比较7的ConcurrentHashMap
8版本的特点是不再采用小数组模式,他直接是数组加链表模式,链表过长也会转成红黑树。初始化的时机。7调用构造方法后,整个数据结构都会创建完成,但8版本调用之后,不会创建,属于懒汉式初始化。
元素个数超过容量3/4触发扩容,hash值重新计算
2、理解capacity和factor
为什么创建出来长度为32,8版本的capacity含义是将来我要放16个元素,所以要创建一个数组长度放下16个元素,但不得超过表容量的3/4,而且必须为2的n次方。
8版本的扩容因子只会在一开始创建时用于计算表长度,其余时间不会用到它。看下图11个元素还没扩容,是固定的3/4才会出现扩容。
3、并发put
8版本里面它是把锁加载每个链表头上,比如下图a处于链表头,之后来的每一个线程都会给a先上锁,如果有其他线程,则会被阻塞,特意设置abc固定hash,b插入时暂停10s,这10s区间c无法完成插入操作的,这个锁只针对这个索引链表头,其他索引不影响。
4、扩容
看一下扩容过程,他先会创建一个32长度表,就是把每个索引的链表迁移到新数组。
首先他会从15号开始,处理完成就会将旧链表头替换成forwardingNode,表示这个处理完,其他线程来了就会知道这个已被处理。10号的元素重新计算hash,不变的迁移到原来位置,变得到新位置
所有节点完成,迁移工作也就完成了
迁移细节问题:
扩容过程其他线程put,get怎么处理?
1、如果这时候来一个get操作,分两种情况,在0-13区间,直接在原表查找,14-15区间,查询线程到新的查找,它是怎么知道去新或旧查找,就通过这个链表头的状态。
2、线程来了要进行一个查询,查询恰好是10索引链表数据,比如查询这个2,链表迁移工作此时未完成,应该在旧的找。但是旧的表1和新表1不是同一个对象,因为hash的重新计算,新的1的next是空,旧的next是2,不会找得到2。所以扩容线程在做链表在迁移时,一般创建一个新的链表对象,因为链表指向发生改变。当然ConcurrentHashMap有做优化,如果最后几个元素hash后相同,则不会创建新的对象。
3、put分三种情况:(1)put线程刚好put到了正在迁移的链表的前半部分0-9位置这时候能够并发执行(2)put线程刚好是10索引位置,那他会阻塞,因为扩容线程会对链表头加锁(3)put线程到11-15,他不能到新的表中put,新的表要等所有迁移完成才可以put,这时候put线程可以帮忙处理迁移行为,每个线程可以处理16个索引。
。