统计过程中,忽略特殊处理过程,譬如:当大于Integer.MaxValue()-8时、当小于默认值时等等等。每个容器特殊处理不一样。这里不做多余赘述,是为了帮助博主以及读者们整理一般情况下,常见容器的扩容策略的记忆而已
文章中几个相关名词可以做个科普:
- 扩容因子:0~1之间,当实际填充占总容量多少时,触发扩容动作
- 扩容方式:譬如:2倍。则按照新的容量newSize为oldSize*2
目录
1.5.4.3、TransferQueue和TransferStack的一些源码比较和思考
1、Collection
1.1、List
1.1.1、ArrayList
默认大小10,增加1.5倍,扩容因子:申请不到新的空间
这里需要稍加留意,作为最基础的容器,在java11后,有本质区别,具体是哪个版本开始修改的,作者也没有去深究,这是11的源码,扩容因子:1,扩容方式+1
1.1.2、Vector
这里有一个capactityIncrement的值,是通过构造器Vector(int,int)中第二个param传入,若是不指定,默认为0。默认情况下,当满了就2倍扩容
1.1.3、CopyOnWriteArrayList
这也是一个蛮重要的并发容器,但他的扩容方式仿佛并不是那么高效,虽然它足够的安全,不过也足以使得我们认清他的定位了。
初始0
扩容因子是满了就扩,而扩容方式,+1
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触发扩容有两种情况,1、当某个节点 > 7时,并且tab长度小于64时,这是不会转换红黑树,而是扩容。2、当tab的填充率达到扩容因子时
(同时,红黑树转换链表的untreeify方法,也是在resize中触发,当桶中树节点小于等于6时拆解)
1.3.2、LinkedHashMap
这里LinkedHashMap也不多做赘述,他的本质是继承了HashMap在那基础之上所作的修改,节点继承,增加新的指针,newNode修改,生成新的拓展节点类
1.3.3、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思想类似,链表转为红黑树