1.ArrayList/Vector
ArrayList:
1.扩建不够扩大为原来的1.5倍(elementData = Arrays.copyOf(elementData, newCapacity);)
2.结构本身由动态数组实现不能序列化(由transient修饰),因此序列化时是对里面的每个元素遍历序列化。
Vector:
1.Vector 底层数据结构和 ArrayList 类似,也是一个动态数组存放数据。不过是在 add() 方法的时候使用 synchronized 进行同步写数据,但是开销较大,所以 Vector 是一个同步容器并不是一个并发容器。
(同步并发:https://blog.csdn.net/XM_no_homework/article/details/103888289)
2.LinkedList
LinkedList 底层是基于双向链表实现的
LinkedList 插入,删除都是移动指针效率很高。
查找需要进行遍历查询,效率较低。
3.HashMap
HashMap 底层是基于数组和链表实现的。其中有两个重要的参数:
- 容量
- 负载因子
容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整)。
hash冲突:
由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成链表,采用头插法将数据插入到链表中。
put()和get():
get 和 put 类似,也是将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equals(k) 来找到对应的元素。
在 JDK1.8 中对 HashMap 进行了优化: 当 hash 碰撞之后写入链表的长度超过了阈值(默认为8)并且 table 的长度不小于64(否则扩容一次)时,链表将会转换为红黑树。
假设 hash 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 O(n) 。
如果是红黑树,时间复杂度就是 O(logn) 。
大大提高了查询效率
4. HashSet
HashSet 是一个不允许存储重复元素的集合,内部由HashMap实现
HashSet成员变量:
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
其中key为set中添加的值,value为 (private static final Object PRESENT = new Object();)
当有重复的值写入到 HashSet 时,value 会被覆盖,但 key 不会受到影响,这样就保证了 HashSet 中只能存放不重复的元素。
5. LinkedHashMap
LinkedHashMap 基于 HashMap, 但具有顺序,来解决有排序需求的场景。
它的底层是继承于 HashMap 实现的,由一个双向链表所构成。
LinkedHashMap 的排序方式有两种:
根据写入顺序排序。
根据访问顺序排序。
其中根据访问顺序排序时,每次 get 都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。
LinkedHashMap 其实就是对 HashMap 进行了拓展,使用了双向链表来保证了顺序性
6.ConcurrentHashMap
结构图:
ConcurrentHashMap由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
对Segment(分段)加锁(继承于Reentrantlock) 先访问segment再访问HashEntry
/**
* Segment 数组,存放数据时首先需要定位到具体的 Segment 中。
*/
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
put()
put()加分段锁,先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
put()过程:
- 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
- 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
- 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
- 最后会解除在 1 中所获取当前 Segment 的锁
get()
get()不加锁,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁
JDK1.8中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。