统计过程中,忽略特殊处理过程,譬如:当大于Integer.MaxValue()-8时、当小于默认值时等等等。每个容器特殊处理不一样。这里不做多余赘述,是为了帮助博主以及读者们整理一般情况下,常见容器的扩容策略的记忆而已
文章中几个相关名词可以做个科普:
- 扩容因子:0~1之间,当实际填充占总容量多少时,触发扩容动作
- 扩容方式:譬如:2倍。则按照新的容量newSize为oldSize*2
目录
1、Collection
1.1、List
1.1.1、ArrayList
默认大小10,增加1.5倍,扩容因子:申请不到新的空间
1.1.2、Vector
这里有一个capactityIncrement的值,是通过构造器Vector(int,int)中第二个param传入,若是不指定,默认为0。默认情况下,当满了就2倍扩容
1.2、Set
1.2.1、HashSet
众所周知,HashSet的实现方式其实就是以HashMap作为状态变量以实现的。所以,这里不做赘述,参照:HashMap
1.2.2、LinkedHashSet
众所周知,HashSet的实现方式其实就是以LinkedHashMap作为状态变量以实现的。所以,这里不做赘述,参照:HashMap
1.3、map
1.3.1、HashMap
这里有很多复杂的处理,当然我们只查看hash表部分扩容策略,并且也只在意常规状态下的扩容。这里默认扩容因子为0.75,扩容方式为2倍扩容,默认大小为16。注意!HashMap不仅是分布占缩少扩容因子会扩容,在红黑树转换时也会检查扩容,即数组大小小于64时候。
1.3.2、LinkedHashMap
这里LinkedHashMap也不多做赘述,他的本质是继承了HashMap在那基础之上所作的修改,节点继承,增加新的指针,newNode修改,生成新的拓展节点类
1.3.2、ConcurrentHashMap
关于ConcurrentHashMap,博主想了半天,这部分蛮重的,关于源码部分,他的实现很复杂,博主研究了好几天,都未看懂,这里有必要列出来详细叙述:
看源码有几种方式,个人认为比较实用的有三种:一个是边看源码根据经验,大块大块的划分代码块的作用途径,通过看到关键字和算法的方式,比较囫囵吞枣,一般经验丰富的程序猿快速看源码都是这样。第二种时,我边运行极限状态,边进行debug,这种方式优点很明显,每句什么用,一清二楚。第三种是“正面刚”,一行一行死扣,罗列各种情况去一一排除。
这里,我觉得有必要用第三种,毕竟这是一个大家耳熟能详的很重要的一个并发容器类,我们需要假设最坏情况,很多个线程大量并发的执行到每一步,会发生什么。相信,这也是我们在构筑安全的并发方法时也经常做的假设。
首先是数组初始化的时候,他不同于大多容器,是在put时候判断为空初始化的。
初始化:
关于这里,如下图中:
SizeCTL是个蛮重点的状态变量,可以看到,通过原子的CAS初始为-1,初始容量为DEFAULT_CAPATITY,即是16,截至到这里为止,全程并未同步,并且若是有线程并发的去执行到(sc > 0) ? sc : DEFAULT_CAPATITY这一句时也是有可能的。要么sc=SIZECTl=0,要么sc=SIZECTL=n - (n >>> 2),若是第二个线程来到这里,sc为12,n为12;若是第三个线程到了这里,sc为9,n为9;第四个线程,sc为7,n为7;第五个,sc为6,n为6;第六个,5,5........最后恒定在sc为3,n为3。
看到这里,心里面不禁会问,是不是哪里错了?没错,是错了。以上是比较单细胞的单线程思想去做的理解,也可以说是我们忽略了unsafe的全部作用
U.compareAndSwapInt(this, SIZECTL, sc, -1)
这里不仅保证了SIZECTL必须等于sc时才能进入,也使得SIZECTL内存区域赋值为了-1,简单来说,利用了CAS实现了一个粗略的悲观锁,当有其他参与初始化的线程来到这里时,发现有线程正在进行初始化,那么,便会主动让出cpu执行权,Thread.yield()保证了这一点,若是第二个线程进来时,前一个线程已经运行完毕finally部分,那么也由try中的if语句驱赶出了创建初始化区域。保证了线程安全。
因此,初始化的结论是:默认大小16,并且多线程并发初始化的话,利用CAS加简单的悲观锁,其他线程发现有线程正在初始化的话,便会让出资源,等到某个线程初始化完成。
关于插入没什么好说的,似乎与我们关注的扩容特征无关,他与hashMap思想类似,链表转为红黑树
扩容时机
首先,每次是否扩容的操作,是在插入结束,也即是分段锁操作完成后进行的,实际上他的扩容操作是在transfer方法中,这里没法直接看到,因为这里有两个部分都会触发他,1、当转换为红黑树的时候,防止数据全部堆积在一起,2、addCount时,在最后的调整大小转发节点时触发
这里在节点链表大于8转换红黑树的时,可能触发扩容,也即是treeifyBin方法,当数组长度小于MIN_TREEIFY_CAPACITY时,这个值是64。也就是说,在他确认转换红黑树后,实际转换前,会先做一次扩容操作,他在tryPresize方法中有transfer去再次检查
下面列出的是treeifyBin和addCount调用扩容方法transfer的地方。咋看之下代码相似度蛮高的,要求如初始化时一样,SIZECTL和sc一致时才能扩容,都使用了CAS实现了悲观锁,保证同时进行扩容操作的线程只有一个
扩容时机的结论,当然我们这部分也并为完全揭秘当Map处于一个什么样的状态时候,才触发扩容操作,只是知道了,在红黑树转换以及addCount时,都会转到某个神秘的算法,去判断是否扩容。关于这里,博主水平有限没法强行解读,推荐两种方式,1、阅读《hackes Delight》这本书,或许能轻松解读这一系列位运算,2、强行理解并作大量的实验Debug,这会耗费大量的精力时间,可能能勉强理解,也可能得出一个形而上学的结论。这里,关于那个神秘的算法,代表的意思,推荐第一种方式。
扩容方式
ConcurrentHashMap的扩容实现,实际是在transfer方法中实现的,具体应该关注这个方法,上文提及了在扩容时用了CAS实现的锁操作,使得同一时间只有一个线程能扩容,剩余的,则在他们相应的扩容时机中,或是while循环等待,或是结束。
这里利用了nextTab,生成新的2倍容量数组。到这里已经很明显了,ConcurrentHashMap按照2倍扩容,并且是以另一个状态遍历的数组保证,并且把原数组中节点转发过来,然后,指针修改,并且把nextTab置空。生成新的nextTab过程中,会阻塞等待循环到的节点释放锁。
简单来说,这里用到了一种链接思想,当某两个链接指向同一数据块时,无论通过哪个链接修改数据,其实都ok,类似文件系统中的软连接,innode对应数据块的道理一致。
1.4、Stack
他在java中单独的实现,实际上时继承了Vector的,这里单独拿出来和其他接口并列,完全是因为它的名字,是为了尊重他是一个很基础的数据结构的地位
实际扩容因子,扩容方式,以及默认大小,完全与Vector一致
1.5、Queue
1.5.1、HashMap
2、Dictionary
2.1、Hashtable
初始为11,扩容因子0.75
转载:https://blog.csdn.net/m0_37590135/article/details/101771074