集合类总结

    集合类

1.结合类继承图:

集合类总结

2.ArrayList:动态数组,容量能动态增长,非线程安全。

  1>包含俩重要对象:size和elementData。size是数组的大小,elementData为动态数组Object[ ],通过构造函数ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;elementData的容量默认是10。

  2>当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量= (原始容量x3) / 2 + 1。

3.LinkedList:双向链表,用于堆栈,队列,双向队列,非线程安全。

  1>header和size。size表示链表中节点的个数。header表示链表的表头,是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous、next和element。previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。

  2>linkList实现双向链表,顺序访问速度快,随机访问效率低。

4.Victor:矢量队列,线程安全。

  1>包含三个成员变量:elementData、elementCount和capacityIncrement。elementData是Object[]类型的数组,它保存了添加到Vector中的元素。如果初始化Vector时,没指定elementData的大小,则默认大小10。随着Vector中元素的增加,Vector的容量也会动态增长。elementCount是动态数组的实际大小。capacityIncrement是动态数组的增长系数。如果在创建Vector时指定了capacityIncrement的大小,则每次当Vector中动态数组容量增加的大小都是capacityIncrement。

  2>Vector的线程安全,基本在所有方法上都加了synchronized关键字。

5.HashMap:散列表,存储key-value类型的键值对映射。非线程安全。key,value都可以为null。映射无序。

  1>重要成员变量:table、size、threshold、loadFactor和modCount。table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的key-value键值对都是存储在Entry数组中的。size是HashMap的大小。threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold=容量 * 负载因子,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。loadFactor即负载因子,哈希表在其容量自动增加之前可以达到多满的一种尺度。默认为0.75。modCount是用来实现fail-fast机制的。

6.HashTable:散列表,存储key-value类型的键值对映射。线程安全,key,value都不可以为null,映射无序。

  1>重要成员变量:table、size、threshold、loadFactor和modCount。同HashMap。

  2>HashTable的线程安全,基本在所有方法上都加了synchronized关键字。

7.LinkedHashMap:有序k-v集合。按照插入顺序排序,非线程安全,不允许kv null。

  1>LinkedHashMap底层使用哈希表与双向链表来保存所有元素。重新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向链接列表。

  2>初始化时增加 accessOrder 参数,默认为 false,代表按照插入顺序进行迭代;设置为 true时代表以访问顺序进行迭代。

  3>在进行put等元素操作的时候,LinkedHashMap会对自己维护的双向链表执行相同的操作。

8.TreeMap:有序kv集合,通过红黑树实现。排列顺序根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。非线程安全。

  1>重要成员:root、size和comparator。

  2>root是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。

  3>size是红黑数中节点的个数。comparator是键值排序的比较实现。

9.HashSet:没有重复元素的集合。HashMap实现的,不保证元素的顺序,允许使用null。非线程安全。

10.TreeSet:有序集合。非线程安全。

  1>TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。非线程安全。

11.HashMap底层原理:

  1>常用的两个方法:get和put。比如调用 hashMap.put("apple", 0) ,插入一个Key为“apple"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):index = Hash(“apple”)假定最后计算出的index是2,那么结果如下:

集合类总结

但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:

集合类总结

这个时候就需要链表来解决,每个Entry也是链表的头节点,当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可。需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”

  2>get方法原理。根据key来查value时,首先将key做一次hash映射。得到Index 值,由于hash冲突可能得到多个Entry的值,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”,第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

  3>深入探究。hashMap初始长度为16,每次自动扩展或者初始化扩容时长度必须是2的幂。之所以选择16是为了服务于从key到index的hash算法。通过利用Key的HashCode值来做某种运算,实现一个尽量均匀分布的Hash函数,采用的是位运算。如何进行位运算呢?有如下的公式(Length是HashMap的长度):
index = HashCode(Key) & (Length - 1)

下面我们以值为“book”的Key来演示整个过程:1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

12.高并发下的HashMap:

  1>HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。

  2>影响发生Resize的因素有两个:

1.Capacity

HashMap的当前长度。

2.LoadFactor

HashMap负载因子,默认值为0.75f。 

衡量HashMap是否进行Resize的条件如下:

HashMap.Size >= Capacity * LoadFactor

rezie方法经过两个步骤:1.扩容:创建一个新的Entry数组长度是以前的两倍。2.rehash: 回顾hash公式:index = HashCode(Key) & (Length - 1),当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。

  3>1.Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是:HashMap.Size >= Capacity * LoadFactor。2.Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

13.线程安全的CurrentHashMap:

  1>关键概念:Segment:Segment本身就相当于一个HashMap对象。Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。结构如下:

集合类总结

像这样的Segment对象在CurrentHashMap里有2的N次方个,共同保存在一个Segments数组中。因此,整个CurrentHashMap结构如下:

集合类总结

ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。这样的二级结构,和数据库的水平拆分有些相似。CurrentHashMap的优势是采用了锁分段技术,每个Segment相当于一个自治区,读写高度自治。Segment之间互不影响。不同Segment的写入是可以并发执行的。同一Segment的写和读是可以并发执行的。Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

  2>读写过程:

Get方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.再次通过hash值,定位到Segment当中数组的具体位置。

Put方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.获取可重入锁
4.再次通过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。

结论:每次读写需要二次定位,首先定位到Segment,再定位到Segment的具体下标。

  3>size操作:

ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:
1.遍历所有的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束。

结论:乐观锁悲观锁的思想:为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才转为悲观锁,锁住所有Segment保证强一致性。

相关推荐